@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/CHANGELOG.md +15 -1
- package/README.md +75 -2
- package/dist/a2a-server.cjs +13 -1
- package/dist/a2a-server.cjs.map +1 -1
- package/dist/a2a-server.d.cts +1 -1
- package/dist/a2a-server.d.ts +1 -1
- package/dist/a2a-server.js +1 -1
- package/dist/{chunk-TYRJBHLM.js → chunk-XMUCELMH.js} +146 -26
- package/dist/chunk-XMUCELMH.js.map +1 -0
- package/dist/{client-CeWCSsmD.d.cts → client-DHwh8MPj.d.cts} +500 -3
- package/dist/{client-CeWCSsmD.d.ts → client-DHwh8MPj.d.ts} +500 -3
- package/dist/index.cjs +436 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -48
- package/dist/index.d.ts +3 -48
- package/dist/index.js +287 -2
- package/dist/index.js.map +1 -1
- package/docs/agent-runs-protocol.md +113 -6
- package/docs/oauth.md +356 -0
- package/docs/wire-protocol.md +1102 -0
- package/package.json +1 -1
- package/dist/chunk-TYRJBHLM.js.map +0 -1
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,
|
|
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.
|
|
55
|
+
declare const SDK_VERSION = "0.10.0";
|
|
101
56
|
|
|
102
|
-
export {
|
|
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,
|
|
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.
|
|
55
|
+
declare const SDK_VERSION = "0.10.0";
|
|
101
56
|
|
|
102
|
-
export {
|
|
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-
|
|
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.
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAS,cAAc;AACvB,SAAS,YAAY,mBAAmB;AAIjC,IAAM,yBAAyB;AAG/B,IAAM,0BAA0B;AAchC,IAAM,mBAAN,cAA+B,YAAY;AAAA,EACvC;AAAA,EACA;AAAA,EAET,YACE,YACA,uBACA,QACA;AACA,UAAM,UAAU,wBACZ,SAAS,UAAU,KAAK,qBAAqB,KAC7C,SAAS,UAAU;AACvB,UAAM,SAAS,EAAE,MAAM,YAAY,OAAO,CAAC;AAC3C,SAAK,OAAO;AACZ,SAAK,aAAa;AAClB,SAAK,wBAAwB;AAAA,EAC/B;AACF;AAkHO,IAAM,oBAAN,MAAwB;AAAA,EACpB;AAAA,EACA;AAAA,EACQ;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,MAAgC;AAC1C,QAAI,CAAC,KAAK,UAAU;AAClB,YAAM,IAAI,YAAY,8CAA8C;AAAA,IACtE;AACA,QAAI,CAAC,KAAK,cAAc;AACtB,YAAM,IAAI,YAAY,kDAAkD;AAAA,IAC1E;AACA,UAAM,IAAI,KAAK,SAAS,WAAW;AACnC,QAAI,OAAO,MAAM,YAAY;AAC3B,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,SAAK,WAAW,KAAK;AACrB,SAAK,eAAe,KAAK;AACzB,SAAK,WAAW,KAAK,WAAW,wBAAwB,QAAQ,QAAQ,EAAE;AAC1E,SAAK,YAAY;AACjB,SAAK,YAAY,KAAK,aAAa;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,0BAA0B,MAA6D;AAC3F,WAAO,KAAK,MAAM;AAAA,MAChB,YAAY;AAAA,MACZ,MAAM,KAAK;AAAA,MACX,cAAc,KAAK;AAAA,MACnB,eAAe,KAAK;AAAA,IACtB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,QAAQ,MAA2C;AACvD,QAAI,CAAC,KAAK,cAAc;AACtB,YAAM,IAAI,YAAY,0DAA0D;AAAA,IAClF;AACA,UAAM,OAA+B;AAAA,MACnC,YAAY;AAAA,MACZ,eAAe,KAAK;AAAA,IACtB;AACA,UAAM,QAAQ,eAAe,KAAK,KAAK;AACvC,QAAI,UAAU,OAAW,MAAK,QAAQ;AACtC,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,kBAAkB,OAAiC,CAAC,GAAwB;AAChF,UAAM,OAA+B;AAAA,MACnC,YAAY;AAAA,IACd;AACA,UAAM,QAAQ,eAAe,KAAK,KAAK;AACvC,QAAI,UAAU,OAAW,MAAK,QAAQ;AACtC,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,OAAO,MAAoC;AAC/C,QAAI,CAAC,KAAK,OAAO;AACf,YAAM,IAAI,YAAY,kDAAkD;AAAA,IAC1E;AACA,UAAM,KAAK,SAAS,qBAAqB;AAAA,MACvC,OAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,mBAAmB,MAA8C;AAC/D,QAAI,CAAC,KAAK,cAAc;AACtB,YAAM,IAAI,YAAY,qEAAqE;AAAA,IAC7F;AACA,UAAM,OAAO,KAAK,iBAAiB;AACnC,UAAM,QAAoB,EAAE,OAAO,KAAK,cAAc,UAAU,KAAK;AACrE,UAAM,eAAe,KAAK;AAC1B,WAAO,gBAAgB,OAAO,MAAM,YAAY;AAC9C,aAAO,KAAK,QAAQ,EAAE,cAAc,OAAO,KAAK,MAAM,CAAC;AAAA,IACzD,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,6BAA6B,OAA4C,CAAC,GAAgB;AACxF,UAAM,OAAO,KAAK,iBAAiB;AACnC,UAAM,QAAoB,EAAE,OAAO,QAAW,UAAU,KAAK;AAC7D,WAAO,gBAAgB,OAAO,MAAM,YAAY;AAC9C,aAAO,KAAK,kBAAkB,EAAE,OAAO,KAAK,MAAM,CAAC;AAAA,IACrD,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,MAAM,MAAmD;AACrE,UAAM,MAAM,MAAM,KAAK,SAAS,oBAAoB,IAAI;AACxD,QAAI,SAAkC,CAAC;AACvC,QAAI;AACF,eAAU,MAAM,IAAI,KAAK;AAAA,IAC3B,QAAQ;AACN,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA,IAAI;AAAA,MACN;AAAA,IACF;AACA,UAAM,cAAc,OAAO,OAAO,iBAAiB,WAAW,OAAO,eAAe;AACpF,QAAI,CAAC,aAAa;AAChB,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA,IAAI;AAAA,MACN;AAAA,IACF;AACA,UAAM,YAAY,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;AAC9E,WAAO;AAAA,MACL;AAAA,MACA,cAAc,OAAO,OAAO,kBAAkB,WAAW,OAAO,gBAAgB;AAAA,MAChF,WAAW,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;AAAA,MACvE;AAAA,MACA,WAAW,KAAK,IAAI,IAAI,YAAY;AAAA,MACpC,OAAO,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AAAA,IAC3D;AAAA,EACF;AAAA,EAEA,MAAc,SAAS,MAAc,MAAiD;AACpF,UAAM,MAAM,GAAG,KAAK,OAAO,GAAG,IAAI;AAClC,UAAM,SAAS,IAAI,gBAAgB;AAAA,MACjC,GAAG;AAAA,MACH,WAAW,KAAK;AAAA,MAChB,eAAe,KAAK;AAAA,IACtB,CAAC;AACD,UAAM,OAAO,IAAI,gBAAgB;AACjC,UAAM,IAAI,WAAW,MAAM,KAAK,MAAM,GAAG,KAAK,SAAS;AACvD,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,KAAK,UAAU,KAAK;AAAA,QAC9B,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,QAAQ;AAAA,QACV;AAAA,QACA,MAAM,OAAO,SAAS;AAAA,QACtB,QAAQ,KAAK;AAAA,MACf,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,UAAI,KAAK,OAAO,SAAS;AACvB,cAAM,IAAI,mBAAmB,iCAAiC,KAAK,SAAS,IAAI;AAAA,MAClF;AACA,YAAM,IAAI,mBAAmB,wBAAyB,IAAc,OAAO,IAAI;AAAA,QAC7E,OAAO;AAAA,MACT,CAAC;AAAA,IACH,UAAE;AACA,mBAAa,CAAC;AAAA,IAChB;AACA,QAAI,CAAC,IAAI,IAAI;AACX,UAAI,UAA4D,CAAC;AACjE,UAAI;AACF,kBAAW,MAAM,IAAI,KAAK;AAAA,MAC5B,QAAQ;AAAA,MAER;AACA,YAAM,aAAa,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ,QAAQ,IAAI,MAAM;AACzF,YAAM,OACJ,OAAO,QAAQ,sBAAsB,WAAW,QAAQ,oBAAoB;AAC9E,YAAM,IAAI,iBAAiB,YAAY,MAAM,IAAI,MAAM;AAAA,IACzD;AACA,WAAO;AAAA,EACT;AACF;AAcO,SAAS,qBAAqB,SAAS,IAAY;AACxD,MAAI,SAAS,MAAM,SAAS,KAAK;AAC/B,UAAM,IAAI,YAAY,gDAAgD;AAAA,EACxE;AAGA,QAAM,WAAW;AACjB,QAAM,QAAQ,YAAY,MAAM;AAChC,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,WAAO,SAAS,MAAM,CAAC,IAAK,SAAS,MAAM;AAAA,EAC7C;AACA,SAAO;AACT;AAMO,SAAS,cAAc,UAA0B;AACtD,QAAM,OAAO,WAAW,QAAQ,EAAE,OAAO,UAAU,MAAM,EAAE,OAAO;AAClE,SAAO,OAAO,KAAK,IAAI,EACpB,SAAS,QAAQ,EACjB,QAAQ,OAAO,EAAE,EACjB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG;AACvB;AAoBA,SAAS,gBACP,OACA,QACA,MACa;AACb,SAAO,OAAO,SAA6B,cAA+B;AACxE,QAAI,WAAW,kBAAkB,MAAM,SAAS,CAAC,WAAW,MAAM,OAAO,MAAM,GAAG;AAChF,aAAO,MAAM,MAAM;AAAA,IACrB;AACA,QAAI,MAAM,UAAU;AAClB,YAAM,IAAI,MAAM,MAAM;AAItB,UAAI,WAAW,kBAAkB,MAAM,MAAM,OAAO;AAAA,MAEpD,OAAO;AACL,eAAO,EAAE;AAAA,MACX;AAAA,IACF;AACA,UAAM,WAAW,KAAK,EAAE;AAAA,MACtB,CAAC,MAAM;AACL,cAAM,QAAQ;AACd,eAAO;AAAA,MACT;AAAA,MACA,CAAC,QAAiB;AAChB,cAAM;AAAA,MACR;AAAA,IACF;AACA,QAAI;AACF,YAAM,IAAI,MAAM,MAAM;AACtB,aAAO,EAAE;AAAA,IACX,UAAE;AACA,YAAM,WAAW;AAAA,IACnB;AAAA,EACF;AACF;AAEA,SAAS,WAAW,OAAmB,QAAyB;AAC9D,SAAO,MAAM,YAAY,KAAK,IAAI,KAAK;AACzC;AAEA,SAAS,eAAe,OAAmE;AACzF,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,UAAU,MAAM,KAAK;AAC3B,WAAO,QAAQ,SAAS,IAAI,UAAU;AAAA,EACxC;AACA,QAAM,SAAS,MAAM,OAAO,CAAC,MAAM,OAAO,MAAM,YAAY,EAAE,SAAS,CAAC,EAAE,KAAK,GAAG;AAClF,SAAO,OAAO,SAAS,IAAI,SAAS;AACtC;;;ACjfO,IAAM,cAAc;","names":[]}
|
|
@@ -66,17 +66,124 @@ All SDK-facing endpoints sit under
|
|
|
66
66
|
/api/v1/workspaces/{workspaceSlug}/...
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
and
|
|
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 <
|
|
73
|
+
Authorization: Bearer <credential>
|
|
73
74
|
# or, equivalently:
|
|
74
|
-
X-API-Key: <
|
|
75
|
+
X-API-Key: <credential>
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|