@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.cjs
CHANGED
|
@@ -32,17 +32,23 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AgentSession: () => AgentSession,
|
|
34
34
|
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
35
|
+
DEFAULT_OAUTH_BASE_URL: () => DEFAULT_OAUTH_BASE_URL,
|
|
36
|
+
DEFAULT_REFRESH_SKEW_MS: () => DEFAULT_REFRESH_SKEW_MS,
|
|
35
37
|
MantyxAuthError: () => MantyxAuthError,
|
|
36
38
|
MantyxClient: () => MantyxClient,
|
|
37
39
|
MantyxError: () => MantyxError,
|
|
38
40
|
MantyxNetworkError: () => MantyxNetworkError,
|
|
41
|
+
MantyxOAuthClient: () => MantyxOAuthClient,
|
|
42
|
+
MantyxOAuthError: () => MantyxOAuthError,
|
|
39
43
|
MantyxParseError: () => MantyxParseError,
|
|
40
44
|
MantyxRunError: () => MantyxRunError,
|
|
45
|
+
MantyxScopeError: () => MantyxScopeError,
|
|
41
46
|
MantyxToolError: () => MantyxToolError,
|
|
42
47
|
SDK_VERSION: () => SDK_VERSION,
|
|
43
48
|
defineLocalA2A: () => defineLocalA2A,
|
|
44
49
|
defineLocalMcp: () => defineLocalMcp,
|
|
45
50
|
defineLocalTool: () => defineLocalTool,
|
|
51
|
+
generatePkceVerifier: () => generatePkceVerifier,
|
|
46
52
|
isLocalA2ATool: () => isLocalA2ATool,
|
|
47
53
|
isLocalMcpServer: () => isLocalMcpServer,
|
|
48
54
|
isLocalTool: () => isLocalTool,
|
|
@@ -51,6 +57,7 @@ __export(index_exports, {
|
|
|
51
57
|
mantyxPluginTool: () => mantyxPluginTool,
|
|
52
58
|
mantyxTool: () => mantyxTool,
|
|
53
59
|
parseRunOutput: () => parseRunOutput,
|
|
60
|
+
pkceChallenge: () => pkceChallenge,
|
|
54
61
|
readSseStream: () => readSseStream,
|
|
55
62
|
toToolParametersWire: () => toToolParametersWire,
|
|
56
63
|
zodToJsonSchema: () => zodToJsonSchema
|
|
@@ -80,11 +87,23 @@ var MantyxNetworkError = class extends MantyxError {
|
|
|
80
87
|
}
|
|
81
88
|
};
|
|
82
89
|
var MantyxAuthError = class extends MantyxError {
|
|
83
|
-
constructor(message = "Invalid or missing API key") {
|
|
90
|
+
constructor(message = "Invalid or missing API key / OAuth access token") {
|
|
84
91
|
super(message, { code: "unauthorized", status: 401 });
|
|
85
92
|
this.name = "MantyxAuthError";
|
|
86
93
|
}
|
|
87
94
|
};
|
|
95
|
+
var MantyxScopeError = class extends MantyxError {
|
|
96
|
+
/**
|
|
97
|
+
* Scope(s) the route demanded. Always at least one entry; usually
|
|
98
|
+
* exactly one. New routes may demand more scopes in the future.
|
|
99
|
+
*/
|
|
100
|
+
requiredScopes;
|
|
101
|
+
constructor(message, requiredScopes) {
|
|
102
|
+
super(message, { code: "insufficient_scope", status: 403 });
|
|
103
|
+
this.name = "MantyxScopeError";
|
|
104
|
+
this.requiredScopes = [...requiredScopes];
|
|
105
|
+
}
|
|
106
|
+
};
|
|
88
107
|
var MantyxToolError = class extends MantyxError {
|
|
89
108
|
toolName;
|
|
90
109
|
constructor(toolName, message) {
|
|
@@ -98,11 +117,23 @@ var MantyxToolError = class extends MantyxError {
|
|
|
98
117
|
var MantyxRunError = class extends MantyxError {
|
|
99
118
|
runId;
|
|
100
119
|
subtype;
|
|
101
|
-
|
|
120
|
+
/** See {@link MantyxRunErrorInit.errorClass}. */
|
|
121
|
+
errorClass;
|
|
122
|
+
/** See {@link MantyxRunErrorInit.finishReason}. */
|
|
123
|
+
finishReason;
|
|
124
|
+
/** See {@link MantyxRunErrorInit.partialText}. */
|
|
125
|
+
partialText;
|
|
126
|
+
/** See {@link MantyxRunErrorInit.retryable}. */
|
|
127
|
+
retryable;
|
|
128
|
+
constructor(runId, subtype, message, init = {}) {
|
|
102
129
|
super(message, { code: subtype });
|
|
103
130
|
this.name = "MantyxRunError";
|
|
104
131
|
this.runId = runId;
|
|
105
132
|
this.subtype = subtype;
|
|
133
|
+
this.errorClass = init.errorClass;
|
|
134
|
+
this.finishReason = init.finishReason;
|
|
135
|
+
this.partialText = init.partialText;
|
|
136
|
+
this.retryable = init.retryable;
|
|
106
137
|
}
|
|
107
138
|
};
|
|
108
139
|
var MantyxParseError = class extends MantyxError {
|
|
@@ -623,9 +654,7 @@ var DEFAULT_BASE_URL = "https://app.mantyx.io";
|
|
|
623
654
|
var MantyxClient = class {
|
|
624
655
|
options;
|
|
625
656
|
constructor(opts) {
|
|
626
|
-
|
|
627
|
-
throw new MantyxError("apiKey is required");
|
|
628
|
-
}
|
|
657
|
+
const { credential, tokenSource } = resolveCredential(opts);
|
|
629
658
|
if (!opts.workspaceSlug || typeof opts.workspaceSlug !== "string") {
|
|
630
659
|
throw new MantyxError("workspaceSlug is required");
|
|
631
660
|
}
|
|
@@ -636,11 +665,12 @@ var MantyxClient = class {
|
|
|
636
665
|
);
|
|
637
666
|
}
|
|
638
667
|
this.options = {
|
|
639
|
-
apiKey:
|
|
668
|
+
apiKey: credential,
|
|
640
669
|
workspaceSlug: opts.workspaceSlug,
|
|
641
670
|
baseUrl: (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
642
671
|
fetch: f,
|
|
643
|
-
timeoutMs: opts.timeoutMs ?? 6e4
|
|
672
|
+
timeoutMs: opts.timeoutMs ?? 6e4,
|
|
673
|
+
tokenSource
|
|
644
674
|
};
|
|
645
675
|
}
|
|
646
676
|
// -------------------------------------------------------------- Models
|
|
@@ -758,7 +788,13 @@ var MantyxClient = class {
|
|
|
758
788
|
}
|
|
759
789
|
} else if (ev.type === "error") {
|
|
760
790
|
const e = ev;
|
|
761
|
-
|
|
791
|
+
const subtype = e.errorClass ?? e.code ?? "error";
|
|
792
|
+
throw new MantyxRunError(runId, subtype, e.error, {
|
|
793
|
+
...e.errorClass !== void 0 ? { errorClass: e.errorClass } : {},
|
|
794
|
+
...e.finishReason !== void 0 ? { finishReason: e.finishReason } : {},
|
|
795
|
+
...typeof e.partialText === "string" ? { partialText: e.partialText } : {},
|
|
796
|
+
...typeof e.retryable === "boolean" ? { retryable: e.retryable } : {}
|
|
797
|
+
});
|
|
762
798
|
} else if (ev.type === "cancelled") {
|
|
763
799
|
throw new MantyxRunError(runId, "cancelled", "Run was cancelled");
|
|
764
800
|
}
|
|
@@ -770,19 +806,7 @@ var MantyxClient = class {
|
|
|
770
806
|
let lastSeq = 0;
|
|
771
807
|
while (true) {
|
|
772
808
|
const reqUrl = lastSeq > 0 ? `${url}?lastSeq=${lastSeq}` : url;
|
|
773
|
-
const res = await this.
|
|
774
|
-
method: "GET",
|
|
775
|
-
headers: {
|
|
776
|
-
...this.authHeaders(),
|
|
777
|
-
Accept: "text/event-stream",
|
|
778
|
-
...lastSeq > 0 ? { "Last-Event-ID": String(lastSeq) } : {}
|
|
779
|
-
},
|
|
780
|
-
...signal ? { signal } : {}
|
|
781
|
-
}).catch((err) => {
|
|
782
|
-
throw new MantyxNetworkError(`Failed to open SSE stream: ${err.message}`, {
|
|
783
|
-
cause: err
|
|
784
|
-
});
|
|
785
|
-
});
|
|
809
|
+
const res = await this.openSseStream(reqUrl, lastSeq, signal);
|
|
786
810
|
if (!res.ok) {
|
|
787
811
|
throw await this.errorFromResponse(res);
|
|
788
812
|
}
|
|
@@ -885,18 +909,72 @@ var MantyxClient = class {
|
|
|
885
909
|
absoluteUrl(path) {
|
|
886
910
|
return `${this.options.baseUrl}/api/v1/workspaces/${encodeURIComponent(this.options.workspaceSlug)}${path}`;
|
|
887
911
|
}
|
|
888
|
-
|
|
889
|
-
|
|
912
|
+
/**
|
|
913
|
+
* Resolve the bearer credential to send on the next request. With a
|
|
914
|
+
* static `apiKey` / `accessToken` this is a synchronous reach into
|
|
915
|
+
* `options.apiKey`; with a {@link TokenSource} it delegates so the
|
|
916
|
+
* source can refresh expired access tokens before we hit the wire.
|
|
917
|
+
*
|
|
918
|
+
* The `reason` is forwarded to the source verbatim. Pass
|
|
919
|
+
* `"unauthorized"` immediately after a 401 so the source forces a
|
|
920
|
+
* refresh rather than handing back its (now-invalid) cached value.
|
|
921
|
+
*/
|
|
922
|
+
async resolveBearer(reason = "initial") {
|
|
923
|
+
if (this.options.tokenSource) return this.options.tokenSource(reason);
|
|
924
|
+
return this.options.apiKey;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Open an SSE stream against `reqUrl` with at-most-one refresh +
|
|
928
|
+
* retry on 401. The caller is responsible for the subsequent
|
|
929
|
+
* `readSseStream` loop; this helper only handles the initial GET.
|
|
930
|
+
* Mid-stream 401s propagate as `MantyxNetworkError` from the read
|
|
931
|
+
* loop and trigger a reconnect via the outer `while` in
|
|
932
|
+
* {@link streamRunEvents}.
|
|
933
|
+
*/
|
|
934
|
+
async openSseStream(reqUrl, lastSeq, signal) {
|
|
935
|
+
const openOnce = async (reason) => {
|
|
936
|
+
const auth = await this.authHeaders(reason);
|
|
937
|
+
return this.options.fetch(reqUrl, {
|
|
938
|
+
method: "GET",
|
|
939
|
+
headers: {
|
|
940
|
+
...auth,
|
|
941
|
+
Accept: "text/event-stream",
|
|
942
|
+
...lastSeq > 0 ? { "Last-Event-ID": String(lastSeq) } : {}
|
|
943
|
+
},
|
|
944
|
+
...signal ? { signal } : {}
|
|
945
|
+
}).catch((err) => {
|
|
946
|
+
throw new MantyxNetworkError(`Failed to open SSE stream: ${err.message}`, {
|
|
947
|
+
cause: err
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
};
|
|
951
|
+
const res = await openOnce("initial");
|
|
952
|
+
if (res.status === 401 && this.options.tokenSource !== null) {
|
|
953
|
+
try {
|
|
954
|
+
await res.text();
|
|
955
|
+
} catch {
|
|
956
|
+
}
|
|
957
|
+
return openOnce("unauthorized");
|
|
958
|
+
}
|
|
959
|
+
return res;
|
|
960
|
+
}
|
|
961
|
+
async authHeaders(reason = "initial") {
|
|
962
|
+
const bearer = await this.resolveBearer(reason);
|
|
963
|
+
return { Authorization: `Bearer ${bearer}` };
|
|
890
964
|
}
|
|
891
965
|
async request(args) {
|
|
966
|
+
return this.requestWithRetry(args, "initial");
|
|
967
|
+
}
|
|
968
|
+
async requestWithRetry(args, reason) {
|
|
892
969
|
const url = this.absoluteUrl(args.path);
|
|
893
970
|
const ctrl = new AbortController();
|
|
894
971
|
const t = setTimeout(() => ctrl.abort(), args.timeoutMs ?? this.options.timeoutMs);
|
|
895
972
|
try {
|
|
973
|
+
const auth = await this.authHeaders(reason);
|
|
896
974
|
const res = await this.options.fetch(url, {
|
|
897
975
|
method: args.method,
|
|
898
976
|
headers: {
|
|
899
|
-
...
|
|
977
|
+
...auth,
|
|
900
978
|
...args.body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
901
979
|
Accept: "application/json"
|
|
902
980
|
},
|
|
@@ -909,6 +987,14 @@ var MantyxClient = class {
|
|
|
909
987
|
throw new MantyxNetworkError(`Network error: ${err.message}`, { cause: err });
|
|
910
988
|
});
|
|
911
989
|
if (!res.ok) {
|
|
990
|
+
if (res.status === 401 && this.options.tokenSource !== null && reason === "initial") {
|
|
991
|
+
try {
|
|
992
|
+
await res.text();
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
clearTimeout(t);
|
|
996
|
+
return this.requestWithRetry(args, "unauthorized");
|
|
997
|
+
}
|
|
912
998
|
throw await this.errorFromResponse(res);
|
|
913
999
|
}
|
|
914
1000
|
const text = await res.text();
|
|
@@ -929,7 +1015,12 @@ var MantyxClient = class {
|
|
|
929
1015
|
} catch {
|
|
930
1016
|
}
|
|
931
1017
|
if (res.status === 401) {
|
|
932
|
-
return new MantyxAuthError(body.error ?? "Invalid API key");
|
|
1018
|
+
return new MantyxAuthError(body.error ?? "Invalid API key or OAuth access token");
|
|
1019
|
+
}
|
|
1020
|
+
if (res.status === 403 && (body.error === "insufficient_scope" || body.code === "insufficient_scope")) {
|
|
1021
|
+
const required = parseRequiredScopes(body.required, res.headers.get("WWW-Authenticate"));
|
|
1022
|
+
const msg = required.length > 0 ? `Missing OAuth scope${required.length > 1 ? "s" : ""}: ${required.join(", ")}` : "OAuth access token is missing a required scope";
|
|
1023
|
+
return new MantyxScopeError(msg, required);
|
|
933
1024
|
}
|
|
934
1025
|
return new MantyxError(body.error ?? `HTTP ${res.status}`, {
|
|
935
1026
|
code: body.code ?? `http_${res.status}`,
|
|
@@ -1295,24 +1386,342 @@ function parseRunOutput(result, validator) {
|
|
|
1295
1386
|
function sleep(ms) {
|
|
1296
1387
|
return new Promise((r) => setTimeout(r, ms));
|
|
1297
1388
|
}
|
|
1389
|
+
function resolveCredential(opts) {
|
|
1390
|
+
const apiKey = typeof opts.apiKey === "string" ? opts.apiKey : "";
|
|
1391
|
+
const accessToken = typeof opts.accessToken === "string" ? opts.accessToken : "";
|
|
1392
|
+
const tokenSource = typeof opts.tokenSource === "function" ? opts.tokenSource : null;
|
|
1393
|
+
const provided = [apiKey ? "apiKey" : "", accessToken ? "accessToken" : "", tokenSource ? "tokenSource" : ""].filter((s) => s.length > 0);
|
|
1394
|
+
if (provided.length > 1) {
|
|
1395
|
+
throw new MantyxError(
|
|
1396
|
+
`Pass exactly one of \`apiKey\`, \`accessToken\`, or \`tokenSource\` \u2014 got ${provided.join(" + ")}.`
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
if (provided.length === 0) {
|
|
1400
|
+
throw new MantyxError(
|
|
1401
|
+
"One of `apiKey` (workspace API key), `accessToken` (OAuth access token), or `tokenSource` (dynamic credential provider) is required"
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
return {
|
|
1405
|
+
credential: apiKey || accessToken,
|
|
1406
|
+
tokenSource
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
function parseRequiredScopes(bodyRequired, wwwAuthenticate) {
|
|
1410
|
+
if (Array.isArray(bodyRequired)) {
|
|
1411
|
+
return bodyRequired.filter((s) => typeof s === "string" && s.length > 0);
|
|
1412
|
+
}
|
|
1413
|
+
if (typeof bodyRequired === "string" && bodyRequired.length > 0) {
|
|
1414
|
+
return [bodyRequired];
|
|
1415
|
+
}
|
|
1416
|
+
if (typeof wwwAuthenticate === "string") {
|
|
1417
|
+
const m = /scope="([^"]+)"/i.exec(wwwAuthenticate);
|
|
1418
|
+
if (m && m[1]) {
|
|
1419
|
+
return m[1].split(/\s+/).filter((s) => s.length > 0);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return [];
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// src/oauth.ts
|
|
1426
|
+
var import_node_buffer = require("buffer");
|
|
1427
|
+
var import_node_crypto = require("crypto");
|
|
1428
|
+
var DEFAULT_OAUTH_BASE_URL = "https://app.mantyx.io";
|
|
1429
|
+
var DEFAULT_REFRESH_SKEW_MS = 6e4;
|
|
1430
|
+
var MantyxOAuthError = class extends MantyxError {
|
|
1431
|
+
oauthError;
|
|
1432
|
+
oauthErrorDescription;
|
|
1433
|
+
constructor(oauthError, oauthErrorDescription, status) {
|
|
1434
|
+
const message = oauthErrorDescription ? `OAuth ${oauthError}: ${oauthErrorDescription}` : `OAuth ${oauthError}`;
|
|
1435
|
+
super(message, { code: oauthError, status });
|
|
1436
|
+
this.name = "MantyxOAuthError";
|
|
1437
|
+
this.oauthError = oauthError;
|
|
1438
|
+
this.oauthErrorDescription = oauthErrorDescription;
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
var MantyxOAuthClient = class {
|
|
1442
|
+
clientId;
|
|
1443
|
+
baseUrl;
|
|
1444
|
+
clientSecret;
|
|
1445
|
+
fetchImpl;
|
|
1446
|
+
timeoutMs;
|
|
1447
|
+
constructor(opts) {
|
|
1448
|
+
if (!opts.clientId) {
|
|
1449
|
+
throw new MantyxError("`clientId` is required for MantyxOAuthClient");
|
|
1450
|
+
}
|
|
1451
|
+
if (!opts.clientSecret) {
|
|
1452
|
+
throw new MantyxError("`clientSecret` is required for MantyxOAuthClient");
|
|
1453
|
+
}
|
|
1454
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
1455
|
+
if (typeof f !== "function") {
|
|
1456
|
+
throw new MantyxError(
|
|
1457
|
+
"Global fetch is not available; pass a custom `fetch` implementation in MantyxOAuthClientOptions."
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
this.clientId = opts.clientId;
|
|
1461
|
+
this.clientSecret = opts.clientSecret;
|
|
1462
|
+
this.baseUrl = (opts.baseUrl ?? DEFAULT_OAUTH_BASE_URL).replace(/\/+$/, "");
|
|
1463
|
+
this.fetchImpl = f;
|
|
1464
|
+
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Swap an authorization-code + PKCE verifier for the initial
|
|
1468
|
+
* `{access_token, refresh_token}` pair. Call this exactly once per
|
|
1469
|
+
* sign-in after the browser/native redirect lands back on your
|
|
1470
|
+
* `redirectUri` with a `code` parameter. Persist the returned
|
|
1471
|
+
* `refreshToken` against the user record — it is long-lived and
|
|
1472
|
+
* non-rotating per `docs/oauth.md` §"Token lifetimes & lifecycle".
|
|
1473
|
+
*/
|
|
1474
|
+
async exchangeAuthorizationCode(opts) {
|
|
1475
|
+
return this.token({
|
|
1476
|
+
grant_type: "authorization_code",
|
|
1477
|
+
code: opts.code,
|
|
1478
|
+
redirect_uri: opts.redirectUri,
|
|
1479
|
+
code_verifier: opts.codeVerifier
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Mint a fresh access token from a stored refresh token. The
|
|
1484
|
+
* returned `refreshToken` is identical to the input — the field is
|
|
1485
|
+
* surfaced for symmetry with {@link exchangeAuthorizationCode} only.
|
|
1486
|
+
*
|
|
1487
|
+
* On `400 invalid_grant` the refresh token has been revoked (or its
|
|
1488
|
+
* grant / app was deleted); the SDK surfaces a
|
|
1489
|
+
* {@link MantyxOAuthError} and callers must drive a fresh sign-in.
|
|
1490
|
+
*/
|
|
1491
|
+
async refresh(opts) {
|
|
1492
|
+
if (!opts.refreshToken) {
|
|
1493
|
+
throw new MantyxError("`refreshToken` is required for MantyxOAuthClient.refresh");
|
|
1494
|
+
}
|
|
1495
|
+
const body = {
|
|
1496
|
+
grant_type: "refresh_token",
|
|
1497
|
+
refresh_token: opts.refreshToken
|
|
1498
|
+
};
|
|
1499
|
+
const scope = normalizeScope(opts.scope);
|
|
1500
|
+
if (scope !== void 0) body.scope = scope;
|
|
1501
|
+
return this.token(body);
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Request a workspace-scoped access token without a user via the
|
|
1505
|
+
* `client_credentials` grant. Available only on private OAuth apps
|
|
1506
|
+
* that were registered with `allowsClientCredentials: true`. No
|
|
1507
|
+
* refresh token is issued; re-call this method whenever a new
|
|
1508
|
+
* access token is needed.
|
|
1509
|
+
*/
|
|
1510
|
+
async clientCredentials(opts = {}) {
|
|
1511
|
+
const body = {
|
|
1512
|
+
grant_type: "client_credentials"
|
|
1513
|
+
};
|
|
1514
|
+
const scope = normalizeScope(opts.scope);
|
|
1515
|
+
if (scope !== void 0) body.scope = scope;
|
|
1516
|
+
return this.token(body);
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Revoke an access or refresh token (RFC 7009). The server always
|
|
1520
|
+
* returns 200, even for unknown tokens. Revoking a **refresh**
|
|
1521
|
+
* token kills the refresh and every live access token tied to its
|
|
1522
|
+
* grant; revoking an **access** token kills only that one.
|
|
1523
|
+
*/
|
|
1524
|
+
async revoke(opts) {
|
|
1525
|
+
if (!opts.token) {
|
|
1526
|
+
throw new MantyxError("`token` is required for MantyxOAuthClient.revoke");
|
|
1527
|
+
}
|
|
1528
|
+
await this.formPost("/api/oauth/revoke", {
|
|
1529
|
+
token: opts.token
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Build a long-lived {@link TokenSource} that re-mints access
|
|
1534
|
+
* tokens from the supplied refresh token. Pass the returned source
|
|
1535
|
+
* to `new MantyxClient({ tokenSource, workspaceSlug, ... })`. The
|
|
1536
|
+
* source caches the access token in-memory and refreshes
|
|
1537
|
+
* proactively when the cached value is within `refreshSkewMs` of
|
|
1538
|
+
* `expiresAt`, or eagerly when `MantyxClient` reports a 401.
|
|
1539
|
+
*/
|
|
1540
|
+
refreshTokenSource(opts) {
|
|
1541
|
+
if (!opts.refreshToken) {
|
|
1542
|
+
throw new MantyxError("`refreshToken` is required for MantyxOAuthClient.refreshTokenSource");
|
|
1543
|
+
}
|
|
1544
|
+
const skew = opts.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;
|
|
1545
|
+
const cache = { token: opts.initialToken, inflight: null };
|
|
1546
|
+
const refreshToken = opts.refreshToken;
|
|
1547
|
+
return makeTokenSource(cache, skew, async () => {
|
|
1548
|
+
return this.refresh({ refreshToken, scope: opts.scope });
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Build a long-lived {@link TokenSource} backed by the
|
|
1553
|
+
* `client_credentials` grant. On every refresh the source re-mints
|
|
1554
|
+
* a workspace-scoped access token by calling the token endpoint
|
|
1555
|
+
* with `grant_type=client_credentials`. Available only on private
|
|
1556
|
+
* apps with `allowsClientCredentials: true`.
|
|
1557
|
+
*/
|
|
1558
|
+
clientCredentialsTokenSource(opts = {}) {
|
|
1559
|
+
const skew = opts.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;
|
|
1560
|
+
const cache = { token: void 0, inflight: null };
|
|
1561
|
+
return makeTokenSource(cache, skew, async () => {
|
|
1562
|
+
return this.clientCredentials({ scope: opts.scope });
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
// -------------------------------------------------------------- internals
|
|
1566
|
+
/**
|
|
1567
|
+
* POST `application/x-www-form-urlencoded` to `/api/oauth/token` and
|
|
1568
|
+
* decode the {@link OAuthToken} response. Always injects `client_id`
|
|
1569
|
+
* + `client_secret` from the constructor.
|
|
1570
|
+
*/
|
|
1571
|
+
async token(body) {
|
|
1572
|
+
const res = await this.formPost("/api/oauth/token", body);
|
|
1573
|
+
let parsed = {};
|
|
1574
|
+
try {
|
|
1575
|
+
parsed = await res.json();
|
|
1576
|
+
} catch {
|
|
1577
|
+
throw new MantyxOAuthError(
|
|
1578
|
+
"invalid_response",
|
|
1579
|
+
"Token endpoint returned a non-JSON response",
|
|
1580
|
+
res.status
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
const accessToken = typeof parsed.access_token === "string" ? parsed.access_token : "";
|
|
1584
|
+
if (!accessToken) {
|
|
1585
|
+
throw new MantyxOAuthError(
|
|
1586
|
+
"invalid_response",
|
|
1587
|
+
"Token endpoint response is missing `access_token`",
|
|
1588
|
+
res.status
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
const expiresIn = typeof parsed.expires_in === "number" ? parsed.expires_in : 3600;
|
|
1592
|
+
return {
|
|
1593
|
+
accessToken,
|
|
1594
|
+
refreshToken: typeof parsed.refresh_token === "string" ? parsed.refresh_token : void 0,
|
|
1595
|
+
tokenType: typeof parsed.token_type === "string" ? parsed.token_type : "Bearer",
|
|
1596
|
+
expiresIn,
|
|
1597
|
+
expiresAt: Date.now() + expiresIn * 1e3,
|
|
1598
|
+
scope: typeof parsed.scope === "string" ? parsed.scope : void 0
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
async formPost(path, body) {
|
|
1602
|
+
const url = `${this.baseUrl}${path}`;
|
|
1603
|
+
const params = new URLSearchParams({
|
|
1604
|
+
...body,
|
|
1605
|
+
client_id: this.clientId,
|
|
1606
|
+
client_secret: this.clientSecret
|
|
1607
|
+
});
|
|
1608
|
+
const ctrl = new AbortController();
|
|
1609
|
+
const t = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
1610
|
+
let res;
|
|
1611
|
+
try {
|
|
1612
|
+
res = await this.fetchImpl(url, {
|
|
1613
|
+
method: "POST",
|
|
1614
|
+
headers: {
|
|
1615
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1616
|
+
Accept: "application/json"
|
|
1617
|
+
},
|
|
1618
|
+
body: params.toString(),
|
|
1619
|
+
signal: ctrl.signal
|
|
1620
|
+
});
|
|
1621
|
+
} catch (err) {
|
|
1622
|
+
if (ctrl.signal.aborted) {
|
|
1623
|
+
throw new MantyxNetworkError(`OAuth request timed out after ${this.timeoutMs}ms`);
|
|
1624
|
+
}
|
|
1625
|
+
throw new MantyxNetworkError(`OAuth network error: ${err.message}`, {
|
|
1626
|
+
cause: err
|
|
1627
|
+
});
|
|
1628
|
+
} finally {
|
|
1629
|
+
clearTimeout(t);
|
|
1630
|
+
}
|
|
1631
|
+
if (!res.ok) {
|
|
1632
|
+
let errBody = {};
|
|
1633
|
+
try {
|
|
1634
|
+
errBody = await res.json();
|
|
1635
|
+
} catch {
|
|
1636
|
+
}
|
|
1637
|
+
const oauthError = typeof errBody.error === "string" ? errBody.error : `http_${res.status}`;
|
|
1638
|
+
const desc = typeof errBody.error_description === "string" ? errBody.error_description : void 0;
|
|
1639
|
+
throw new MantyxOAuthError(oauthError, desc, res.status);
|
|
1640
|
+
}
|
|
1641
|
+
return res;
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
function generatePkceVerifier(length = 64) {
|
|
1645
|
+
if (length < 43 || length > 128) {
|
|
1646
|
+
throw new MantyxError("PKCE code_verifier length must be in [43, 128]");
|
|
1647
|
+
}
|
|
1648
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
1649
|
+
const bytes = (0, import_node_crypto.randomBytes)(length);
|
|
1650
|
+
let out = "";
|
|
1651
|
+
for (let i = 0; i < length; i++) {
|
|
1652
|
+
out += ALPHABET[bytes[i] % ALPHABET.length];
|
|
1653
|
+
}
|
|
1654
|
+
return out;
|
|
1655
|
+
}
|
|
1656
|
+
function pkceChallenge(verifier) {
|
|
1657
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(verifier, "utf8").digest();
|
|
1658
|
+
return import_node_buffer.Buffer.from(hash).toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
1659
|
+
}
|
|
1660
|
+
function makeTokenSource(cache, skewMs, mint) {
|
|
1661
|
+
return async (reason = "initial") => {
|
|
1662
|
+
if (reason !== "unauthorized" && cache.token && !isExpiring(cache.token, skewMs)) {
|
|
1663
|
+
return cache.token.accessToken;
|
|
1664
|
+
}
|
|
1665
|
+
if (cache.inflight) {
|
|
1666
|
+
const t = await cache.inflight;
|
|
1667
|
+
if (reason === "unauthorized" && t === cache.token) {
|
|
1668
|
+
} else {
|
|
1669
|
+
return t.accessToken;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
cache.inflight = mint().then(
|
|
1673
|
+
(t) => {
|
|
1674
|
+
cache.token = t;
|
|
1675
|
+
return t;
|
|
1676
|
+
},
|
|
1677
|
+
(err) => {
|
|
1678
|
+
throw err;
|
|
1679
|
+
}
|
|
1680
|
+
);
|
|
1681
|
+
try {
|
|
1682
|
+
const t = await cache.inflight;
|
|
1683
|
+
return t.accessToken;
|
|
1684
|
+
} finally {
|
|
1685
|
+
cache.inflight = null;
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
function isExpiring(token, skewMs) {
|
|
1690
|
+
return token.expiresAt - Date.now() <= skewMs;
|
|
1691
|
+
}
|
|
1692
|
+
function normalizeScope(scope) {
|
|
1693
|
+
if (scope === void 0) return void 0;
|
|
1694
|
+
if (typeof scope === "string") {
|
|
1695
|
+
const trimmed = scope.trim();
|
|
1696
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1697
|
+
}
|
|
1698
|
+
const joined = scope.filter((s) => typeof s === "string" && s.length > 0).join(" ");
|
|
1699
|
+
return joined.length > 0 ? joined : void 0;
|
|
1700
|
+
}
|
|
1298
1701
|
|
|
1299
1702
|
// src/version.ts
|
|
1300
|
-
var SDK_VERSION = "0.
|
|
1703
|
+
var SDK_VERSION = "0.10.0";
|
|
1301
1704
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1302
1705
|
0 && (module.exports = {
|
|
1303
1706
|
AgentSession,
|
|
1304
1707
|
DEFAULT_BASE_URL,
|
|
1708
|
+
DEFAULT_OAUTH_BASE_URL,
|
|
1709
|
+
DEFAULT_REFRESH_SKEW_MS,
|
|
1305
1710
|
MantyxAuthError,
|
|
1306
1711
|
MantyxClient,
|
|
1307
1712
|
MantyxError,
|
|
1308
1713
|
MantyxNetworkError,
|
|
1714
|
+
MantyxOAuthClient,
|
|
1715
|
+
MantyxOAuthError,
|
|
1309
1716
|
MantyxParseError,
|
|
1310
1717
|
MantyxRunError,
|
|
1718
|
+
MantyxScopeError,
|
|
1311
1719
|
MantyxToolError,
|
|
1312
1720
|
SDK_VERSION,
|
|
1313
1721
|
defineLocalA2A,
|
|
1314
1722
|
defineLocalMcp,
|
|
1315
1723
|
defineLocalTool,
|
|
1724
|
+
generatePkceVerifier,
|
|
1316
1725
|
isLocalA2ATool,
|
|
1317
1726
|
isLocalMcpServer,
|
|
1318
1727
|
isLocalTool,
|
|
@@ -1321,6 +1730,7 @@ var SDK_VERSION = "0.9.0";
|
|
|
1321
1730
|
mantyxPluginTool,
|
|
1322
1731
|
mantyxTool,
|
|
1323
1732
|
parseRunOutput,
|
|
1733
|
+
pkceChallenge,
|
|
1324
1734
|
readSseStream,
|
|
1325
1735
|
toToolParametersWire,
|
|
1326
1736
|
zodToJsonSchema
|