@mantyx/sdk 0.9.0 → 0.10.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/dist/index.d.cts CHANGED
@@ -1,51 +1,6 @@
1
- export { A as A2AToolRef, a as AgentSession, b as AgentSpecBase, c as AssistantDeltaEvent, d as AssistantMessageEvent, C as CancelledEvent, D as DEFAULT_BASE_URL, e as DefineLocalA2AOptions, f as DefineLocalMcpOptions, g as DefineLocalToolOptions, E as ErrorEvent, L as LocalA2ATool, h as LocalHandlers, i as LocalMcpHttpTransport, j as LocalMcpServer, k as LocalMcpStdioTransport, l as LocalTool, m as LocalToolCallEvent, n as LocalToolResultInEvent, o as LoopDetectedEvent, p as LoopDetection, q as MantyxA2AOptions, M as MantyxClient, r as MantyxClientOptions, s as MantyxMcpOptions, t as MantyxPluginToolRef, u as MantyxToolRef, v as McpToolRef, w as ModelCatalog, x as ModelInfo, O as OutputSchema, R as ReasoningLevel, y as ResultEvent, z as RunEvent, B as RunEventBase, F as RunResult, G as RunSpec, S as ServerToolResultEvent, H as SessionInfo, I as SessionSpec, J as ThinkingDeltaEvent, K as ToolBudget, N as ToolBudgetExceededEvent, P as ToolBudgets, T as ToolRef, Z as ZodLikeObject, Q as defineLocalA2A, U as defineLocalMcp, V as defineLocalTool, W as isLocalA2ATool, X as isLocalMcpServer, Y as isLocalTool, _ as mantyxA2A, $ as mantyxMcp, a0 as mantyxPluginTool, a1 as mantyxTool, a2 as parseRunOutput } from './client-CeWCSsmD.cjs';
1
+ export { A as A2AToolRef, a as AgentSession, b as AgentSpecBase, c as AssistantDeltaEvent, d as AssistantMessageEvent, C as CancelledEvent, e as ClientCredentialsOptions, f as ClientCredentialsTokenSourceOptions, D as DEFAULT_BASE_URL, g as DEFAULT_OAUTH_BASE_URL, h as DEFAULT_REFRESH_SKEW_MS, i as DefineLocalA2AOptions, j as DefineLocalMcpOptions, k as DefineLocalToolOptions, E as ErrorEvent, l as ExchangeAuthorizationCodeOptions, L as LocalA2ATool, m as LocalHandlers, n as LocalMcpHttpTransport, o as LocalMcpServer, p as LocalMcpStdioTransport, q as LocalTool, r as LocalToolCallEvent, s as LocalToolResultInEvent, t as LoopDetectedEvent, u as LoopDetection, v as MantyxA2AOptions, w as MantyxAuthError, M as MantyxClient, x as MantyxClientOptions, y as MantyxError, z as MantyxMcpOptions, B as MantyxNetworkError, F as MantyxOAuthClient, G as MantyxOAuthClientOptions, H as MantyxOAuthError, I as MantyxParseError, J as MantyxPluginToolRef, K as MantyxRunError, N as MantyxRunErrorInit, O as MantyxScopeError, P as MantyxToolError, Q as MantyxToolRef, S as McpToolRef, U as ModelCatalog, V as ModelInfo, W as OAuthToken, X as OutputSchema, R as ReasoningLevel, Y as RefreshOptions, Z as RefreshTokenSourceOptions, _ as ResultEvent, $ as RevokeOptions, a0 as RunEvent, a1 as RunEventBase, a2 as RunResult, a3 as RunSpec, a4 as ServerToolResultEvent, a5 as SessionInfo, a6 as SessionSpec, a7 as ThinkingDeltaEvent, a8 as TokenRequestReason, a9 as TokenSource, aa as ToolBudget, ab as ToolBudgetExceededEvent, ac as ToolBudgets, T as ToolRef, ad as ZodLikeObject, ae as defineLocalA2A, af as defineLocalMcp, ag as defineLocalTool, ah as generatePkceVerifier, ai as isLocalA2ATool, aj as isLocalMcpServer, ak as isLocalTool, al as mantyxA2A, am as mantyxMcp, an as mantyxPluginTool, ao as mantyxTool, ap as parseRunOutput, aq as pkceChallenge } from './client-DHwh8MPj.cjs';
2
2
  import { z } from 'zod';
3
3
 
4
- /**
5
- * Error types raised by the MANTYX SDK.
6
- */
7
- declare class MantyxError extends Error {
8
- readonly code: string;
9
- readonly status: number | undefined;
10
- readonly hint: string | undefined;
11
- constructor(message: string, opts?: {
12
- code?: string;
13
- status?: number;
14
- hint?: string;
15
- });
16
- }
17
- declare class MantyxNetworkError extends MantyxError {
18
- constructor(message: string, opts?: {
19
- cause?: unknown;
20
- });
21
- }
22
- declare class MantyxAuthError extends MantyxError {
23
- constructor(message?: string);
24
- }
25
- declare class MantyxToolError extends MantyxError {
26
- readonly toolName: string;
27
- constructor(toolName: string, message: string);
28
- }
29
- declare class MantyxRunError extends MantyxError {
30
- readonly runId: string;
31
- readonly subtype: string;
32
- constructor(runId: string, subtype: string, message: string);
33
- }
34
- /**
35
- * Thrown by {@link parseRunOutput} when the run's terminal text was supposed
36
- * to be a JSON document (because `outputSchema` was set on the spec) but
37
- * either failed to JSON.parse or failed the user-supplied validator.
38
- *
39
- * The original `text` is preserved on the `text` field so callers can log
40
- * the raw model output for debugging.
41
- */
42
- declare class MantyxParseError extends MantyxError {
43
- readonly text: string;
44
- constructor(message: string, text: string, opts?: {
45
- cause?: unknown;
46
- });
47
- }
48
-
49
4
  /**
50
5
  * Lightweight Zod → JSON Schema converter for tool parameter definitions.
51
6
  *
@@ -97,6 +52,6 @@ declare function readSseStream(body: ReadableStream<Uint8Array> | null, opts?: S
97
52
  /**
98
53
  * Release version — synced from repo root VERSION (`npm run sync-version`).
99
54
  */
100
- declare const SDK_VERSION = "0.9.0";
55
+ declare const SDK_VERSION = "0.10.0";
101
56
 
102
- export { MantyxAuthError, MantyxError, MantyxNetworkError, MantyxParseError, MantyxRunError, MantyxToolError, SDK_VERSION, type SseEvent, type SseStreamOptions, readSseStream, toToolParametersWire, zodToJsonSchema };
57
+ export { SDK_VERSION, type SseEvent, type SseStreamOptions, readSseStream, toToolParametersWire, zodToJsonSchema };
package/dist/index.d.ts CHANGED
@@ -1,51 +1,6 @@
1
- export { A as A2AToolRef, a as AgentSession, b as AgentSpecBase, c as AssistantDeltaEvent, d as AssistantMessageEvent, C as CancelledEvent, D as DEFAULT_BASE_URL, e as DefineLocalA2AOptions, f as DefineLocalMcpOptions, g as DefineLocalToolOptions, E as ErrorEvent, L as LocalA2ATool, h as LocalHandlers, i as LocalMcpHttpTransport, j as LocalMcpServer, k as LocalMcpStdioTransport, l as LocalTool, m as LocalToolCallEvent, n as LocalToolResultInEvent, o as LoopDetectedEvent, p as LoopDetection, q as MantyxA2AOptions, M as MantyxClient, r as MantyxClientOptions, s as MantyxMcpOptions, t as MantyxPluginToolRef, u as MantyxToolRef, v as McpToolRef, w as ModelCatalog, x as ModelInfo, O as OutputSchema, R as ReasoningLevel, y as ResultEvent, z as RunEvent, B as RunEventBase, F as RunResult, G as RunSpec, S as ServerToolResultEvent, H as SessionInfo, I as SessionSpec, J as ThinkingDeltaEvent, K as ToolBudget, N as ToolBudgetExceededEvent, P as ToolBudgets, T as ToolRef, Z as ZodLikeObject, Q as defineLocalA2A, U as defineLocalMcp, V as defineLocalTool, W as isLocalA2ATool, X as isLocalMcpServer, Y as isLocalTool, _ as mantyxA2A, $ as mantyxMcp, a0 as mantyxPluginTool, a1 as mantyxTool, a2 as parseRunOutput } from './client-CeWCSsmD.js';
1
+ export { A as A2AToolRef, a as AgentSession, b as AgentSpecBase, c as AssistantDeltaEvent, d as AssistantMessageEvent, C as CancelledEvent, e as ClientCredentialsOptions, f as ClientCredentialsTokenSourceOptions, D as DEFAULT_BASE_URL, g as DEFAULT_OAUTH_BASE_URL, h as DEFAULT_REFRESH_SKEW_MS, i as DefineLocalA2AOptions, j as DefineLocalMcpOptions, k as DefineLocalToolOptions, E as ErrorEvent, l as ExchangeAuthorizationCodeOptions, L as LocalA2ATool, m as LocalHandlers, n as LocalMcpHttpTransport, o as LocalMcpServer, p as LocalMcpStdioTransport, q as LocalTool, r as LocalToolCallEvent, s as LocalToolResultInEvent, t as LoopDetectedEvent, u as LoopDetection, v as MantyxA2AOptions, w as MantyxAuthError, M as MantyxClient, x as MantyxClientOptions, y as MantyxError, z as MantyxMcpOptions, B as MantyxNetworkError, F as MantyxOAuthClient, G as MantyxOAuthClientOptions, H as MantyxOAuthError, I as MantyxParseError, J as MantyxPluginToolRef, K as MantyxRunError, N as MantyxRunErrorInit, O as MantyxScopeError, P as MantyxToolError, Q as MantyxToolRef, S as McpToolRef, U as ModelCatalog, V as ModelInfo, W as OAuthToken, X as OutputSchema, R as ReasoningLevel, Y as RefreshOptions, Z as RefreshTokenSourceOptions, _ as ResultEvent, $ as RevokeOptions, a0 as RunEvent, a1 as RunEventBase, a2 as RunResult, a3 as RunSpec, a4 as ServerToolResultEvent, a5 as SessionInfo, a6 as SessionSpec, a7 as ThinkingDeltaEvent, a8 as TokenRequestReason, a9 as TokenSource, aa as ToolBudget, ab as ToolBudgetExceededEvent, ac as ToolBudgets, T as ToolRef, ad as ZodLikeObject, ae as defineLocalA2A, af as defineLocalMcp, ag as defineLocalTool, ah as generatePkceVerifier, ai as isLocalA2ATool, aj as isLocalMcpServer, ak as isLocalTool, al as mantyxA2A, am as mantyxMcp, an as mantyxPluginTool, ao as mantyxTool, ap as parseRunOutput, aq as pkceChallenge } from './client-DHwh8MPj.js';
2
2
  import { z } from 'zod';
3
3
 
4
- /**
5
- * Error types raised by the MANTYX SDK.
6
- */
7
- declare class MantyxError extends Error {
8
- readonly code: string;
9
- readonly status: number | undefined;
10
- readonly hint: string | undefined;
11
- constructor(message: string, opts?: {
12
- code?: string;
13
- status?: number;
14
- hint?: string;
15
- });
16
- }
17
- declare class MantyxNetworkError extends MantyxError {
18
- constructor(message: string, opts?: {
19
- cause?: unknown;
20
- });
21
- }
22
- declare class MantyxAuthError extends MantyxError {
23
- constructor(message?: string);
24
- }
25
- declare class MantyxToolError extends MantyxError {
26
- readonly toolName: string;
27
- constructor(toolName: string, message: string);
28
- }
29
- declare class MantyxRunError extends MantyxError {
30
- readonly runId: string;
31
- readonly subtype: string;
32
- constructor(runId: string, subtype: string, message: string);
33
- }
34
- /**
35
- * Thrown by {@link parseRunOutput} when the run's terminal text was supposed
36
- * to be a JSON document (because `outputSchema` was set on the spec) but
37
- * either failed to JSON.parse or failed the user-supplied validator.
38
- *
39
- * The original `text` is preserved on the `text` field so callers can log
40
- * the raw model output for debugging.
41
- */
42
- declare class MantyxParseError extends MantyxError {
43
- readonly text: string;
44
- constructor(message: string, text: string, opts?: {
45
- cause?: unknown;
46
- });
47
- }
48
-
49
4
  /**
50
5
  * Lightweight Zod → JSON Schema converter for tool parameter definitions.
51
6
  *
@@ -97,6 +52,6 @@ declare function readSseStream(body: ReadableStream<Uint8Array> | null, opts?: S
97
52
  /**
98
53
  * Release version — synced from repo root VERSION (`npm run sync-version`).
99
54
  */
100
- declare const SDK_VERSION = "0.9.0";
55
+ declare const SDK_VERSION = "0.10.0";
101
56
 
102
- export { MantyxAuthError, MantyxError, MantyxNetworkError, MantyxParseError, MantyxRunError, MantyxToolError, SDK_VERSION, type SseEvent, type SseStreamOptions, readSseStream, toToolParametersWire, zodToJsonSchema };
57
+ export { SDK_VERSION, type SseEvent, type SseStreamOptions, readSseStream, toToolParametersWire, zodToJsonSchema };
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  MantyxNetworkError,
8
8
  MantyxParseError,
9
9
  MantyxRunError,
10
+ MantyxScopeError,
10
11
  MantyxToolError,
11
12
  defineLocalA2A,
12
13
  defineLocalMcp,
@@ -22,24 +23,307 @@ import {
22
23
  readSseStream,
23
24
  toToolParametersWire,
24
25
  zodToJsonSchema
25
- } from "./chunk-TYRJBHLM.js";
26
+ } from "./chunk-XMUCELMH.js";
27
+
28
+ // src/oauth.ts
29
+ import { Buffer } from "buffer";
30
+ import { createHash, randomBytes } from "crypto";
31
+ var DEFAULT_OAUTH_BASE_URL = "https://app.mantyx.io";
32
+ var DEFAULT_REFRESH_SKEW_MS = 6e4;
33
+ var MantyxOAuthError = class extends MantyxError {
34
+ oauthError;
35
+ oauthErrorDescription;
36
+ constructor(oauthError, oauthErrorDescription, status) {
37
+ const message = oauthErrorDescription ? `OAuth ${oauthError}: ${oauthErrorDescription}` : `OAuth ${oauthError}`;
38
+ super(message, { code: oauthError, status });
39
+ this.name = "MantyxOAuthError";
40
+ this.oauthError = oauthError;
41
+ this.oauthErrorDescription = oauthErrorDescription;
42
+ }
43
+ };
44
+ var MantyxOAuthClient = class {
45
+ clientId;
46
+ baseUrl;
47
+ clientSecret;
48
+ fetchImpl;
49
+ timeoutMs;
50
+ constructor(opts) {
51
+ if (!opts.clientId) {
52
+ throw new MantyxError("`clientId` is required for MantyxOAuthClient");
53
+ }
54
+ if (!opts.clientSecret) {
55
+ throw new MantyxError("`clientSecret` is required for MantyxOAuthClient");
56
+ }
57
+ const f = opts.fetch ?? globalThis.fetch;
58
+ if (typeof f !== "function") {
59
+ throw new MantyxError(
60
+ "Global fetch is not available; pass a custom `fetch` implementation in MantyxOAuthClientOptions."
61
+ );
62
+ }
63
+ this.clientId = opts.clientId;
64
+ this.clientSecret = opts.clientSecret;
65
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_OAUTH_BASE_URL).replace(/\/+$/, "");
66
+ this.fetchImpl = f;
67
+ this.timeoutMs = opts.timeoutMs ?? 3e4;
68
+ }
69
+ /**
70
+ * Swap an authorization-code + PKCE verifier for the initial
71
+ * `{access_token, refresh_token}` pair. Call this exactly once per
72
+ * sign-in after the browser/native redirect lands back on your
73
+ * `redirectUri` with a `code` parameter. Persist the returned
74
+ * `refreshToken` against the user record — it is long-lived and
75
+ * non-rotating per `docs/oauth.md` §"Token lifetimes & lifecycle".
76
+ */
77
+ async exchangeAuthorizationCode(opts) {
78
+ return this.token({
79
+ grant_type: "authorization_code",
80
+ code: opts.code,
81
+ redirect_uri: opts.redirectUri,
82
+ code_verifier: opts.codeVerifier
83
+ });
84
+ }
85
+ /**
86
+ * Mint a fresh access token from a stored refresh token. The
87
+ * returned `refreshToken` is identical to the input — the field is
88
+ * surfaced for symmetry with {@link exchangeAuthorizationCode} only.
89
+ *
90
+ * On `400 invalid_grant` the refresh token has been revoked (or its
91
+ * grant / app was deleted); the SDK surfaces a
92
+ * {@link MantyxOAuthError} and callers must drive a fresh sign-in.
93
+ */
94
+ async refresh(opts) {
95
+ if (!opts.refreshToken) {
96
+ throw new MantyxError("`refreshToken` is required for MantyxOAuthClient.refresh");
97
+ }
98
+ const body = {
99
+ grant_type: "refresh_token",
100
+ refresh_token: opts.refreshToken
101
+ };
102
+ const scope = normalizeScope(opts.scope);
103
+ if (scope !== void 0) body.scope = scope;
104
+ return this.token(body);
105
+ }
106
+ /**
107
+ * Request a workspace-scoped access token without a user via the
108
+ * `client_credentials` grant. Available only on private OAuth apps
109
+ * that were registered with `allowsClientCredentials: true`. No
110
+ * refresh token is issued; re-call this method whenever a new
111
+ * access token is needed.
112
+ */
113
+ async clientCredentials(opts = {}) {
114
+ const body = {
115
+ grant_type: "client_credentials"
116
+ };
117
+ const scope = normalizeScope(opts.scope);
118
+ if (scope !== void 0) body.scope = scope;
119
+ return this.token(body);
120
+ }
121
+ /**
122
+ * Revoke an access or refresh token (RFC 7009). The server always
123
+ * returns 200, even for unknown tokens. Revoking a **refresh**
124
+ * token kills the refresh and every live access token tied to its
125
+ * grant; revoking an **access** token kills only that one.
126
+ */
127
+ async revoke(opts) {
128
+ if (!opts.token) {
129
+ throw new MantyxError("`token` is required for MantyxOAuthClient.revoke");
130
+ }
131
+ await this.formPost("/api/oauth/revoke", {
132
+ token: opts.token
133
+ });
134
+ }
135
+ /**
136
+ * Build a long-lived {@link TokenSource} that re-mints access
137
+ * tokens from the supplied refresh token. Pass the returned source
138
+ * to `new MantyxClient({ tokenSource, workspaceSlug, ... })`. The
139
+ * source caches the access token in-memory and refreshes
140
+ * proactively when the cached value is within `refreshSkewMs` of
141
+ * `expiresAt`, or eagerly when `MantyxClient` reports a 401.
142
+ */
143
+ refreshTokenSource(opts) {
144
+ if (!opts.refreshToken) {
145
+ throw new MantyxError("`refreshToken` is required for MantyxOAuthClient.refreshTokenSource");
146
+ }
147
+ const skew = opts.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;
148
+ const cache = { token: opts.initialToken, inflight: null };
149
+ const refreshToken = opts.refreshToken;
150
+ return makeTokenSource(cache, skew, async () => {
151
+ return this.refresh({ refreshToken, scope: opts.scope });
152
+ });
153
+ }
154
+ /**
155
+ * Build a long-lived {@link TokenSource} backed by the
156
+ * `client_credentials` grant. On every refresh the source re-mints
157
+ * a workspace-scoped access token by calling the token endpoint
158
+ * with `grant_type=client_credentials`. Available only on private
159
+ * apps with `allowsClientCredentials: true`.
160
+ */
161
+ clientCredentialsTokenSource(opts = {}) {
162
+ const skew = opts.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;
163
+ const cache = { token: void 0, inflight: null };
164
+ return makeTokenSource(cache, skew, async () => {
165
+ return this.clientCredentials({ scope: opts.scope });
166
+ });
167
+ }
168
+ // -------------------------------------------------------------- internals
169
+ /**
170
+ * POST `application/x-www-form-urlencoded` to `/api/oauth/token` and
171
+ * decode the {@link OAuthToken} response. Always injects `client_id`
172
+ * + `client_secret` from the constructor.
173
+ */
174
+ async token(body) {
175
+ const res = await this.formPost("/api/oauth/token", body);
176
+ let parsed = {};
177
+ try {
178
+ parsed = await res.json();
179
+ } catch {
180
+ throw new MantyxOAuthError(
181
+ "invalid_response",
182
+ "Token endpoint returned a non-JSON response",
183
+ res.status
184
+ );
185
+ }
186
+ const accessToken = typeof parsed.access_token === "string" ? parsed.access_token : "";
187
+ if (!accessToken) {
188
+ throw new MantyxOAuthError(
189
+ "invalid_response",
190
+ "Token endpoint response is missing `access_token`",
191
+ res.status
192
+ );
193
+ }
194
+ const expiresIn = typeof parsed.expires_in === "number" ? parsed.expires_in : 3600;
195
+ return {
196
+ accessToken,
197
+ refreshToken: typeof parsed.refresh_token === "string" ? parsed.refresh_token : void 0,
198
+ tokenType: typeof parsed.token_type === "string" ? parsed.token_type : "Bearer",
199
+ expiresIn,
200
+ expiresAt: Date.now() + expiresIn * 1e3,
201
+ scope: typeof parsed.scope === "string" ? parsed.scope : void 0
202
+ };
203
+ }
204
+ async formPost(path, body) {
205
+ const url = `${this.baseUrl}${path}`;
206
+ const params = new URLSearchParams({
207
+ ...body,
208
+ client_id: this.clientId,
209
+ client_secret: this.clientSecret
210
+ });
211
+ const ctrl = new AbortController();
212
+ const t = setTimeout(() => ctrl.abort(), this.timeoutMs);
213
+ let res;
214
+ try {
215
+ res = await this.fetchImpl(url, {
216
+ method: "POST",
217
+ headers: {
218
+ "Content-Type": "application/x-www-form-urlencoded",
219
+ Accept: "application/json"
220
+ },
221
+ body: params.toString(),
222
+ signal: ctrl.signal
223
+ });
224
+ } catch (err) {
225
+ if (ctrl.signal.aborted) {
226
+ throw new MantyxNetworkError(`OAuth request timed out after ${this.timeoutMs}ms`);
227
+ }
228
+ throw new MantyxNetworkError(`OAuth network error: ${err.message}`, {
229
+ cause: err
230
+ });
231
+ } finally {
232
+ clearTimeout(t);
233
+ }
234
+ if (!res.ok) {
235
+ let errBody = {};
236
+ try {
237
+ errBody = await res.json();
238
+ } catch {
239
+ }
240
+ const oauthError = typeof errBody.error === "string" ? errBody.error : `http_${res.status}`;
241
+ const desc = typeof errBody.error_description === "string" ? errBody.error_description : void 0;
242
+ throw new MantyxOAuthError(oauthError, desc, res.status);
243
+ }
244
+ return res;
245
+ }
246
+ };
247
+ function generatePkceVerifier(length = 64) {
248
+ if (length < 43 || length > 128) {
249
+ throw new MantyxError("PKCE code_verifier length must be in [43, 128]");
250
+ }
251
+ const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
252
+ const bytes = randomBytes(length);
253
+ let out = "";
254
+ for (let i = 0; i < length; i++) {
255
+ out += ALPHABET[bytes[i] % ALPHABET.length];
256
+ }
257
+ return out;
258
+ }
259
+ function pkceChallenge(verifier) {
260
+ const hash = createHash("sha256").update(verifier, "utf8").digest();
261
+ return Buffer.from(hash).toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
262
+ }
263
+ function makeTokenSource(cache, skewMs, mint) {
264
+ return async (reason = "initial") => {
265
+ if (reason !== "unauthorized" && cache.token && !isExpiring(cache.token, skewMs)) {
266
+ return cache.token.accessToken;
267
+ }
268
+ if (cache.inflight) {
269
+ const t = await cache.inflight;
270
+ if (reason === "unauthorized" && t === cache.token) {
271
+ } else {
272
+ return t.accessToken;
273
+ }
274
+ }
275
+ cache.inflight = mint().then(
276
+ (t) => {
277
+ cache.token = t;
278
+ return t;
279
+ },
280
+ (err) => {
281
+ throw err;
282
+ }
283
+ );
284
+ try {
285
+ const t = await cache.inflight;
286
+ return t.accessToken;
287
+ } finally {
288
+ cache.inflight = null;
289
+ }
290
+ };
291
+ }
292
+ function isExpiring(token, skewMs) {
293
+ return token.expiresAt - Date.now() <= skewMs;
294
+ }
295
+ function normalizeScope(scope) {
296
+ if (scope === void 0) return void 0;
297
+ if (typeof scope === "string") {
298
+ const trimmed = scope.trim();
299
+ return trimmed.length > 0 ? trimmed : void 0;
300
+ }
301
+ const joined = scope.filter((s) => typeof s === "string" && s.length > 0).join(" ");
302
+ return joined.length > 0 ? joined : void 0;
303
+ }
26
304
 
27
305
  // src/version.ts
28
- var SDK_VERSION = "0.9.0";
306
+ var SDK_VERSION = "0.10.0";
29
307
  export {
30
308
  AgentSession,
31
309
  DEFAULT_BASE_URL,
310
+ DEFAULT_OAUTH_BASE_URL,
311
+ DEFAULT_REFRESH_SKEW_MS,
32
312
  MantyxAuthError,
33
313
  MantyxClient,
34
314
  MantyxError,
35
315
  MantyxNetworkError,
316
+ MantyxOAuthClient,
317
+ MantyxOAuthError,
36
318
  MantyxParseError,
37
319
  MantyxRunError,
320
+ MantyxScopeError,
38
321
  MantyxToolError,
39
322
  SDK_VERSION,
40
323
  defineLocalA2A,
41
324
  defineLocalMcp,
42
325
  defineLocalTool,
326
+ generatePkceVerifier,
43
327
  isLocalA2ATool,
44
328
  isLocalMcpServer,
45
329
  isLocalTool,
@@ -48,6 +332,7 @@ export {
48
332
  mantyxPluginTool,
49
333
  mantyxTool,
50
334
  parseRunOutput,
335
+ pkceChallenge,
51
336
  readSseStream,
52
337
  toToolParametersWire,
53
338
  zodToJsonSchema
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["/**\n * Release version — synced from repo root VERSION (`npm run sync-version`).\n */\nexport const SDK_VERSION = \"0.9.0\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAGO,IAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/oauth.ts","../src/version.ts"],"sourcesContent":["/**\n * MANTYX OAuth 2.0 client: authorization-code exchange, refresh-token\n * minting, client-credentials grant, and token revocation, plus typed\n * {@link TokenSource}s that {@link MantyxClient} can consume to refresh\n * access tokens transparently before they expire (and again on 401).\n *\n * The wire contract this implements is `docs/oauth.md` in the SDK monorepo:\n *\n * - Token endpoint: `POST <baseUrl>/api/oauth/token`, form-encoded.\n * - Revoke endpoint: `POST <baseUrl>/api/oauth/revoke`, form-encoded.\n * - Access tokens (`mantyx_at_…`) live 1 hour (`expires_in: 3600`).\n * - Refresh tokens (`mantyx_rt_…`) are **persistent and non-rotating**:\n * `grant_type=refresh_token` echoes back the same value the client\n * sent. The caller persists the refresh token once at first sign-in\n * (encrypted at rest) and the SDK re-mints access tokens from it on\n * demand.\n *\n * See also `docs/oauth.md` for the authorization-code + PKCE consent\n * flow (which the SDK does **not** drive — the calling app owns the\n * redirect dance; once it has the auth code, `exchangeAuthorizationCode`\n * swaps it for the initial `{access_token, refresh_token}` pair).\n */\n\nimport { Buffer } from \"node:buffer\";\nimport { createHash, randomBytes } from \"node:crypto\";\n\nimport { MantyxError, MantyxNetworkError } from \"./errors.js\";\n\nexport const DEFAULT_OAUTH_BASE_URL = \"https://app.mantyx.io\";\n\n/** Skew (ms) before `expiresAt` at which a TokenSource will pre-emptively refresh. Default 60s. */\nexport const DEFAULT_REFRESH_SKEW_MS = 60_000;\n\n/**\n * Raised on a non-2xx response from `POST /api/oauth/token` or\n * `POST /api/oauth/revoke`. Carries the RFC 6749 `error` discriminator\n * (`\"invalid_grant\"`, `\"invalid_client\"`, `\"unsupported_grant_type\"`,\n * …) and the optional `error_description` so callers can branch on\n * machine-readable values without parsing the human message.\n *\n * `invalid_grant` from the refresh path specifically signals that the\n * refresh token has been revoked (or the OAuth grant / application\n * was deleted). The SDK never loops on this — callers should route\n * the user back to a fresh sign-in.\n */\nexport class MantyxOAuthError extends MantyxError {\n readonly oauthError: string;\n readonly oauthErrorDescription: string | undefined;\n\n constructor(\n oauthError: string,\n oauthErrorDescription: string | undefined,\n status: number,\n ) {\n const message = oauthErrorDescription\n ? `OAuth ${oauthError}: ${oauthErrorDescription}`\n : `OAuth ${oauthError}`;\n super(message, { code: oauthError, status });\n this.name = \"MantyxOAuthError\";\n this.oauthError = oauthError;\n this.oauthErrorDescription = oauthErrorDescription;\n }\n}\n\n/**\n * Decoded `POST /api/oauth/token` response, augmented with an absolute\n * `expiresAt` timestamp the SDK can use to decide when to refresh.\n *\n * `refreshToken` is present on the initial `authorization_code` exchange\n * and on subsequent `refresh_token` calls (where it is identical to the\n * value the client just sent — refresh tokens never rotate). The\n * `client_credentials` grant never returns one.\n */\nexport interface OAuthToken {\n readonly accessToken: string;\n readonly refreshToken: string | undefined;\n readonly tokenType: string;\n readonly expiresIn: number;\n /** Absolute Unix-ms timestamp set when the SDK parsed the response. */\n readonly expiresAt: number;\n readonly scope: string | undefined;\n}\n\n/** Why the SDK asked the {@link TokenSource} for the current access token. */\nexport type TokenRequestReason = \"initial\" | \"expired\" | \"unauthorized\";\n\n/**\n * A `TokenSource` produces the current access token on demand. The\n * {@link MantyxClient} HTTP layer calls it before every request. When\n * called with `reason: \"unauthorized\"` the source MUST force a refresh\n * (do not return a cached value); this is how the SDK recovers from\n * 401s caused by a token that the server already invalidated.\n *\n * Implementations should be safe to call from many concurrent requests.\n */\nexport type TokenSource = (reason?: TokenRequestReason) => Promise<string>;\n\n/** Caller-supplied options for `MantyxOAuthClient`. */\nexport interface MantyxOAuthClientOptions {\n /**\n * OAuth `client_id` issued at app registration (token prefix\n * `mantyx_oa_`).\n */\n clientId: string;\n /**\n * OAuth `client_secret` issued at app registration (token prefix\n * `mantyx_oas_`). Every MANTYX OAuth app is a confidential client,\n * so this is always required for token + revoke calls. Treat as a\n * deployment secret — do not bundle into browser builds.\n */\n clientSecret: string;\n /**\n * Origin of the MANTYX deployment. Defaults to `https://app.mantyx.io`.\n * The OAuth endpoints are mounted at `<baseUrl>/api/oauth/...`.\n */\n baseUrl?: string;\n /** Optional `fetch` override (e.g. node-fetch wrapper). Default: global `fetch`. */\n fetch?: typeof fetch;\n /** Default per-request timeout in milliseconds. Default: 30s. */\n timeoutMs?: number;\n}\n\nexport interface ExchangeAuthorizationCodeOptions {\n code: string;\n redirectUri: string;\n codeVerifier: string;\n}\n\nexport interface RefreshOptions {\n refreshToken: string;\n /**\n * Optional scope narrowing. Must be a subset of the scopes already\n * granted to the refresh token (server enforces this). Useful when\n * an SDK consumer wants a short-scope access token for a specific\n * sub-operation.\n */\n scope?: string | readonly string[];\n}\n\nexport interface ClientCredentialsOptions {\n scope?: string | readonly string[];\n}\n\nexport interface RevokeOptions {\n token: string;\n}\n\nexport interface RefreshTokenSourceOptions {\n refreshToken: string;\n /** Optional scope narrowing applied on every refresh. */\n scope?: string | readonly string[];\n /**\n * How many ms before `expiresAt` the source proactively refreshes.\n * Defaults to {@link DEFAULT_REFRESH_SKEW_MS} (60s).\n */\n refreshSkewMs?: number;\n /**\n * Optional initial access token + expiry to seed the source's cache\n * with (e.g. the token already in hand from the authorization-code\n * exchange). When omitted, the source mints one on the first call.\n */\n initialToken?: OAuthToken;\n}\n\nexport interface ClientCredentialsTokenSourceOptions {\n scope?: string | readonly string[];\n refreshSkewMs?: number;\n}\n\n/**\n * Wraps the MANTYX OAuth 2.0 authorization-server endpoints. App-scoped\n * (one per `{clientId, clientSecret}` pair); construct independently of\n * {@link MantyxClient}, then either call its grant helpers directly or\n * hand a `TokenSource` it produces to `MantyxClient` for fully\n * transparent refresh.\n */\nexport class MantyxOAuthClient {\n readonly clientId: string;\n readonly baseUrl: string;\n private readonly clientSecret: string;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n\n constructor(opts: MantyxOAuthClientOptions) {\n if (!opts.clientId) {\n throw new MantyxError(\"`clientId` is required for MantyxOAuthClient\");\n }\n if (!opts.clientSecret) {\n throw new MantyxError(\"`clientSecret` is required for MantyxOAuthClient\");\n }\n const f = opts.fetch ?? globalThis.fetch;\n if (typeof f !== \"function\") {\n throw new MantyxError(\n \"Global fetch is not available; pass a custom `fetch` implementation in MantyxOAuthClientOptions.\",\n );\n }\n this.clientId = opts.clientId;\n this.clientSecret = opts.clientSecret;\n this.baseUrl = (opts.baseUrl ?? DEFAULT_OAUTH_BASE_URL).replace(/\\/+$/, \"\");\n this.fetchImpl = f;\n this.timeoutMs = opts.timeoutMs ?? 30_000;\n }\n\n /**\n * Swap an authorization-code + PKCE verifier for the initial\n * `{access_token, refresh_token}` pair. Call this exactly once per\n * sign-in after the browser/native redirect lands back on your\n * `redirectUri` with a `code` parameter. Persist the returned\n * `refreshToken` against the user record — it is long-lived and\n * non-rotating per `docs/oauth.md` §\"Token lifetimes & lifecycle\".\n */\n async exchangeAuthorizationCode(opts: ExchangeAuthorizationCodeOptions): Promise<OAuthToken> {\n return this.token({\n grant_type: \"authorization_code\",\n code: opts.code,\n redirect_uri: opts.redirectUri,\n code_verifier: opts.codeVerifier,\n });\n }\n\n /**\n * Mint a fresh access token from a stored refresh token. The\n * returned `refreshToken` is identical to the input — the field is\n * surfaced for symmetry with {@link exchangeAuthorizationCode} only.\n *\n * On `400 invalid_grant` the refresh token has been revoked (or its\n * grant / app was deleted); the SDK surfaces a\n * {@link MantyxOAuthError} and callers must drive a fresh sign-in.\n */\n async refresh(opts: RefreshOptions): Promise<OAuthToken> {\n if (!opts.refreshToken) {\n throw new MantyxError(\"`refreshToken` is required for MantyxOAuthClient.refresh\");\n }\n const body: Record<string, string> = {\n grant_type: \"refresh_token\",\n refresh_token: opts.refreshToken,\n };\n const scope = normalizeScope(opts.scope);\n if (scope !== undefined) body.scope = scope;\n return this.token(body);\n }\n\n /**\n * Request a workspace-scoped access token without a user via the\n * `client_credentials` grant. Available only on private OAuth apps\n * that were registered with `allowsClientCredentials: true`. No\n * refresh token is issued; re-call this method whenever a new\n * access token is needed.\n */\n async clientCredentials(opts: ClientCredentialsOptions = {}): Promise<OAuthToken> {\n const body: Record<string, string> = {\n grant_type: \"client_credentials\",\n };\n const scope = normalizeScope(opts.scope);\n if (scope !== undefined) body.scope = scope;\n return this.token(body);\n }\n\n /**\n * Revoke an access or refresh token (RFC 7009). The server always\n * returns 200, even for unknown tokens. Revoking a **refresh**\n * token kills the refresh and every live access token tied to its\n * grant; revoking an **access** token kills only that one.\n */\n async revoke(opts: RevokeOptions): Promise<void> {\n if (!opts.token) {\n throw new MantyxError(\"`token` is required for MantyxOAuthClient.revoke\");\n }\n await this.formPost(\"/api/oauth/revoke\", {\n token: opts.token,\n });\n }\n\n /**\n * Build a long-lived {@link TokenSource} that re-mints access\n * tokens from the supplied refresh token. Pass the returned source\n * to `new MantyxClient({ tokenSource, workspaceSlug, ... })`. The\n * source caches the access token in-memory and refreshes\n * proactively when the cached value is within `refreshSkewMs` of\n * `expiresAt`, or eagerly when `MantyxClient` reports a 401.\n */\n refreshTokenSource(opts: RefreshTokenSourceOptions): TokenSource {\n if (!opts.refreshToken) {\n throw new MantyxError(\"`refreshToken` is required for MantyxOAuthClient.refreshTokenSource\");\n }\n const skew = opts.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;\n const cache: TokenCache = { token: opts.initialToken, inflight: null };\n const refreshToken = opts.refreshToken;\n return makeTokenSource(cache, skew, async () => {\n return this.refresh({ refreshToken, scope: opts.scope });\n });\n }\n\n /**\n * Build a long-lived {@link TokenSource} backed by the\n * `client_credentials` grant. On every refresh the source re-mints\n * a workspace-scoped access token by calling the token endpoint\n * with `grant_type=client_credentials`. Available only on private\n * apps with `allowsClientCredentials: true`.\n */\n clientCredentialsTokenSource(opts: ClientCredentialsTokenSourceOptions = {}): TokenSource {\n const skew = opts.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;\n const cache: TokenCache = { token: undefined, inflight: null };\n return makeTokenSource(cache, skew, async () => {\n return this.clientCredentials({ scope: opts.scope });\n });\n }\n\n // -------------------------------------------------------------- internals\n\n /**\n * POST `application/x-www-form-urlencoded` to `/api/oauth/token` and\n * decode the {@link OAuthToken} response. Always injects `client_id`\n * + `client_secret` from the constructor.\n */\n private async token(body: Record<string, string>): Promise<OAuthToken> {\n const res = await this.formPost(\"/api/oauth/token\", body);\n let parsed: Record<string, unknown> = {};\n try {\n parsed = (await res.json()) as Record<string, unknown>;\n } catch {\n throw new MantyxOAuthError(\n \"invalid_response\",\n \"Token endpoint returned a non-JSON response\",\n res.status,\n );\n }\n const accessToken = typeof parsed.access_token === \"string\" ? parsed.access_token : \"\";\n if (!accessToken) {\n throw new MantyxOAuthError(\n \"invalid_response\",\n \"Token endpoint response is missing `access_token`\",\n res.status,\n );\n }\n const expiresIn = typeof parsed.expires_in === \"number\" ? parsed.expires_in : 3600;\n return {\n accessToken,\n refreshToken: typeof parsed.refresh_token === \"string\" ? parsed.refresh_token : undefined,\n tokenType: typeof parsed.token_type === \"string\" ? parsed.token_type : \"Bearer\",\n expiresIn,\n expiresAt: Date.now() + expiresIn * 1000,\n scope: typeof parsed.scope === \"string\" ? parsed.scope : undefined,\n };\n }\n\n private async formPost(path: string, body: Record<string, string>): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n const params = new URLSearchParams({\n ...body,\n client_id: this.clientId,\n client_secret: this.clientSecret,\n });\n const ctrl = new AbortController();\n const t = setTimeout(() => ctrl.abort(), this.timeoutMs);\n let res: Response;\n try {\n res = await this.fetchImpl(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body: params.toString(),\n signal: ctrl.signal,\n });\n } catch (err) {\n if (ctrl.signal.aborted) {\n throw new MantyxNetworkError(`OAuth request timed out after ${this.timeoutMs}ms`);\n }\n throw new MantyxNetworkError(`OAuth network error: ${(err as Error).message}`, {\n cause: err,\n });\n } finally {\n clearTimeout(t);\n }\n if (!res.ok) {\n let errBody: { error?: unknown; error_description?: unknown } = {};\n try {\n errBody = (await res.json()) as typeof errBody;\n } catch {\n // ignore\n }\n const oauthError = typeof errBody.error === \"string\" ? errBody.error : `http_${res.status}`;\n const desc =\n typeof errBody.error_description === \"string\" ? errBody.error_description : undefined;\n throw new MantyxOAuthError(oauthError, desc, res.status);\n }\n return res;\n }\n}\n\n// -------------------------------------------------------------- PKCE helpers\n\n/**\n * Generate a high-entropy PKCE `code_verifier` (RFC 7636 §4.1). The\n * verifier is the raw secret you keep across the redirect; the\n * `code_challenge` you send on `/api/oauth/authorize` is derived from\n * it via {@link pkceChallenge}.\n *\n * Default length is 64 characters (≈ 384 bits of entropy after\n * base64url-encoding the 32 random bytes). Pass `length` to clamp to\n * the RFC's 43..128 inclusive range.\n */\nexport function generatePkceVerifier(length = 64): string {\n if (length < 43 || length > 128) {\n throw new MantyxError(\"PKCE code_verifier length must be in [43, 128]\");\n }\n // 32 random bytes -> 43 base64url chars; we then slice / pad up to the\n // requested length using the unreserved RFC 7636 alphabet.\n const ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\";\n const bytes = randomBytes(length);\n let out = \"\";\n for (let i = 0; i < length; i++) {\n out += ALPHABET[bytes[i]! % ALPHABET.length];\n }\n return out;\n}\n\n/**\n * Compute the PKCE `S256` `code_challenge` for a given verifier:\n * `base64url(sha256(verifier))` with no padding (RFC 7636 §4.2).\n */\nexport function pkceChallenge(verifier: string): string {\n const hash = createHash(\"sha256\").update(verifier, \"utf8\").digest();\n return Buffer.from(hash)\n .toString(\"base64\")\n .replace(/=+$/, \"\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\");\n}\n\n// -------------------------------------------------------------- internals\n\ninterface TokenCache {\n token: OAuthToken | undefined;\n inflight: Promise<OAuthToken> | null;\n}\n\n/**\n * Wrap a `mintToken` thunk into a single-flight {@link TokenSource}\n * with a cache + proactive-refresh skew. The cache is overwritten\n * atomically on every successful mint; the in-flight promise\n * collapses N concurrent expired-token observers into one mint call.\n *\n * Single-flight is an efficiency, not a correctness requirement —\n * `docs/oauth.md` explicitly allows multiple concurrent refreshes\n * against the same refresh token — but it keeps the token-endpoint\n * QPS reasonable when an SDK consumer fans out work in parallel.\n */\nfunction makeTokenSource(\n cache: TokenCache,\n skewMs: number,\n mint: () => Promise<OAuthToken>,\n): TokenSource {\n return async (reason: TokenRequestReason = \"initial\"): Promise<string> => {\n if (reason !== \"unauthorized\" && cache.token && !isExpiring(cache.token, skewMs)) {\n return cache.token.accessToken;\n }\n if (cache.inflight) {\n const t = await cache.inflight;\n // If the inflight refresh was triggered by a benign cache miss\n // and we observed an unauthorized hint after it started, retry\n // once with a forced mint so the caller never gets a stale token.\n if (reason === \"unauthorized\" && t === cache.token) {\n // fallthrough to fresh mint below\n } else {\n return t.accessToken;\n }\n }\n cache.inflight = mint().then(\n (t) => {\n cache.token = t;\n return t;\n },\n (err: unknown) => {\n throw err;\n },\n );\n try {\n const t = await cache.inflight;\n return t.accessToken;\n } finally {\n cache.inflight = null;\n }\n };\n}\n\nfunction isExpiring(token: OAuthToken, skewMs: number): boolean {\n return token.expiresAt - Date.now() <= skewMs;\n}\n\nfunction normalizeScope(scope: string | readonly string[] | undefined): string | undefined {\n if (scope === undefined) return undefined;\n if (typeof scope === \"string\") {\n const trimmed = scope.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n }\n const joined = scope.filter((s) => typeof s === \"string\" && s.length > 0).join(\" \");\n return joined.length > 0 ? joined : undefined;\n}\n","/**\n * Release version — synced from repo root VERSION (`npm run sync-version`).\n */\nexport const SDK_VERSION = \"0.10.0\";\n"],"mappings":"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","names":[]}
@@ -66,17 +66,124 @@ All SDK-facing endpoints sit under
66
66
  /api/v1/workspaces/{workspaceSlug}/...
67
67
  ```
68
68
 
69
- and are authenticated with a workspace API key with usage `developer_api`:
69
+ and accept **either** of two bearer credentials interchangeably. The same
70
+ header carries either, so SDKs only need one code path:
70
71
 
71
72
  ```
72
- Authorization: Bearer <api-key>
73
+ Authorization: Bearer <credential>
73
74
  # or, equivalently:
74
- X-API-Key: <api-key>
75
+ X-API-Key: <credential>
75
76
  ```
76
77
 
77
- The workspace slug in the URL must match the key's tenant. Mismatches return
78
- `404 not_found`. Missing/invalid keys return `401 unauthorized`. Rate limits
79
- follow the workspace's existing developer-API sliding-window policy.
78
+ | Credential | Token format | Identifies | Bound to | Use when |
79
+ | ------------------------- | --------------- | ------------------------ | ----------------------- | -------- |
80
+ | **Workspace API key** | `mantyx_…` | The workspace | One workspace, no end-user | Personal scripts, internal automations, anything the SDK caller owns end-to-end. |
81
+ | **OAuth 2.0 access token**| `mantyx_at_…` | An end user **and** the workspace they consented for | One workspace, one user (or one app for `client_credentials`) | "Sign in with MANTYX" apps, third-party integrations, anywhere consent + scopes matter. |
82
+
83
+ The server resolves whichever it sees by token-prefix sniffing (see
84
+ `packages/api/src/services/bearer-credential.ts`) — SDKs do **not** need
85
+ separate code paths or env variables for the two flavours.
86
+
87
+ The workspace slug in the URL must match the credential's tenant.
88
+ Mismatches return `404 not_found` with a `hint` field pointing at the
89
+ correct slug. Missing/invalid credentials return `401 unauthorized`.
90
+ Rate limits follow the workspace's existing developer-API sliding-window
91
+ policy and are tracked per-credential.
92
+
93
+ ### 2.1 Workspace API keys (machine credentials)
94
+
95
+ A workspace admin issues an API key under **Settings → API keys** with
96
+ **Usage = Developer API**. The key inherits two optional restrictions:
97
+
98
+ - **Agent allowlist** (`ApiKey.agentIds`) — empty list = "every
99
+ non-system agent in the workspace"; otherwise only the listed agents
100
+ are visible to `spec.agentId` and ephemeral runs created from the key.
101
+ - **Plan gate** — the workspace tier must include the `apiKeys` feature.
102
+
103
+ API keys carry no granular scopes; possession of a Developer-API key is
104
+ enough to call every route in this document.
105
+
106
+ ### 2.2 OAuth 2.0 access tokens
107
+
108
+ OAuth tokens are a drop-in alternative for the same set of routes, with
109
+ two differences:
110
+
111
+ 1. **Scopes are required.** Each route checks the token carries the
112
+ right scope via `requireScope(...)` and returns
113
+ `403 { "error": "insufficient_scope", "required": "runs:write" }`
114
+ (the value is a string for single-scope routes, an array for
115
+ multi-scope ones — see §2.3). The SDK is expected to surface this
116
+ verbatim. The agent-runs surface uses these scopes:
117
+
118
+ | Endpoint | Required scope |
119
+ | ------------------------------------------------------------ | -------------- |
120
+ | `GET .../models` | `models:read` |
121
+ | `POST .../agent-runs` | `runs:write` |
122
+ | `GET .../agent-runs/{runId}` | `runs:read` |
123
+ | `GET .../agent-runs/{runId}/stream` | `runs:read` |
124
+ | `POST .../agent-runs/{runId}/cancel` | `runs:write` |
125
+ | `POST .../agent-runs/{runId}/tool-results` | `runs:write` |
126
+ | `POST .../agent-sessions` | `sessions:write` |
127
+ | `GET .../agent-sessions/{sessionId}` | `sessions:read` |
128
+ | `DELETE .../agent-sessions/{sessionId}` | `sessions:write` |
129
+ | `POST .../agent-sessions/{sessionId}/messages` | `sessions:write` |
130
+ | `GET /api/oauth/userinfo` | `mantyx.identity:read` |
131
+
132
+ For an SDK that exposes one-shot runs and sessions end-to-end, request
133
+ at minimum `models:read runs:read runs:write sessions:read sessions:write`,
134
+ and add `mantyx.identity:read` if the SDK calls
135
+ `/api/oauth/userinfo` to discover the workspace slug after sign-in.
136
+
137
+ 2. **Tokens are workspace-scoped.** An access token is minted for one
138
+ workspace (chosen by the user at consent time for public apps, or the
139
+ registering workspace for private apps). Calling
140
+ `/api/v1/workspaces/{otherSlug}/...` with such a token returns
141
+ `404 not_found` plus a `hint` with the correct slug.
142
+
143
+ OAuth tokens **also** honor the per-token agent allow-list
144
+ (`OAuthAccessToken.agentIds`) the user picked at consent time — see
145
+ [`docs/oauth.md`](./oauth.md) for the full registration / authorization-code
146
+ + PKCE flow. PKCE (`S256`) is mandatory and every MANTYX OAuth app is a
147
+ confidential client, so the token endpoint requires both `client_secret`
148
+ and `code_verifier`.
149
+
150
+ **Token lifetimes.** Access tokens live **1 hour** (`expires_in: 3600`).
151
+ Refresh tokens are **persistent and non-rotating**: they have no
152
+ time-based expiry and `grant_type=refresh_token` returns the **same**
153
+ refresh token the SDK already holds while minting a brand-new short-lived
154
+ access token. Multiple processes may refresh concurrently using the same
155
+ refresh token without invalidating each other. Refresh tokens stop
156
+ working only when the application access is revoked (`/oauth/revoke`,
157
+ `DELETE /api/oauth/grants/:id`, or app deletion).
158
+
159
+ > **SDK guidance.** Persist the refresh token at first sign-in, treat it
160
+ > as long-lived, and keep refreshing the access token off it on demand
161
+ > (e.g. ~5 minutes before `expires_in` runs out, or lazily on the first
162
+ > `401`). Do **not** rotate or replace the refresh token after each
163
+ > refresh — the value is stable.
164
+
165
+ A single SDK call site looks identical regardless of credential:
166
+
167
+ ```http
168
+ POST /api/v1/workspaces/acme/agent-runs HTTP/1.1
169
+ Authorization: Bearer mantyx_at_… # OAuth access token
170
+ # — or —
171
+ Authorization: Bearer mantyx_… # workspace API key
172
+ Content-Type: application/json
173
+
174
+ { "modelId": "openai:gpt-5.5", "prompt": "...", "tools": [...] }
175
+ ```
176
+
177
+ ### 2.3 Error model for credentials
178
+
179
+ | Status | Body shape | When |
180
+ | ------ | ------------------------------------------------------------------------------------- | ---- |
181
+ | `401` | `{ "error": "Unauthorized", "message": "API key or OAuth access token required..." }` | No `Authorization` / `X-API-Key` header. |
182
+ | `401` | `{ "error": "Invalid API key or OAuth access token" }` | Token doesn't match a row, expired, or revoked. |
183
+ | `403` | `{ "error": "This API key is not for the Developer API", "hint": "..." }` | API key has wrong `usage`. |
184
+ | `403` | `{ "error": "Workspace API keys are not available on this plan.", "code": "api_keys_plan" }` <br> `{ "error": "OAuth applications are not available on this plan.", "code": "oauth_apps_plan" }` | Workspace tier lacks the `apiKeys` / `oauthApps` feature. |
185
+ | `403` | `{ "error": "insufficient_scope", "required": "runs:write" }` (or an array if a route needs multiple) | OAuth token is missing a scope a route demands. The response also sets `WWW-Authenticate: Bearer error="insufficient_scope", scope="..."`. |
186
+ | `404` | `{ "error": "Workspace path does not match this credential", "hint": "..." }` | URL slug ≠ token's workspace. |
80
187
 
81
188
  ## 3. Models
82
189