@mantyx/sdk 0.9.1 → 0.10.1

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.cjs CHANGED
@@ -32,12 +32,17 @@ 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,
@@ -80,11 +85,23 @@ var MantyxNetworkError = class extends MantyxError {
80
85
  }
81
86
  };
82
87
  var MantyxAuthError = class extends MantyxError {
83
- constructor(message = "Invalid or missing API key") {
88
+ constructor(message = "Invalid or missing API key / OAuth access token") {
84
89
  super(message, { code: "unauthorized", status: 401 });
85
90
  this.name = "MantyxAuthError";
86
91
  }
87
92
  };
93
+ var MantyxScopeError = class extends MantyxError {
94
+ /**
95
+ * Scope(s) the route demanded. Always at least one entry; usually
96
+ * exactly one. New routes may demand more scopes in the future.
97
+ */
98
+ requiredScopes;
99
+ constructor(message, requiredScopes) {
100
+ super(message, { code: "insufficient_scope", status: 403 });
101
+ this.name = "MantyxScopeError";
102
+ this.requiredScopes = [...requiredScopes];
103
+ }
104
+ };
88
105
  var MantyxToolError = class extends MantyxError {
89
106
  toolName;
90
107
  constructor(toolName, message) {
@@ -635,9 +652,7 @@ var DEFAULT_BASE_URL = "https://app.mantyx.io";
635
652
  var MantyxClient = class {
636
653
  options;
637
654
  constructor(opts) {
638
- if (!opts.apiKey || typeof opts.apiKey !== "string") {
639
- throw new MantyxError("apiKey is required");
640
- }
655
+ const { credential, tokenSource } = resolveCredential(opts);
641
656
  if (!opts.workspaceSlug || typeof opts.workspaceSlug !== "string") {
642
657
  throw new MantyxError("workspaceSlug is required");
643
658
  }
@@ -648,11 +663,12 @@ var MantyxClient = class {
648
663
  );
649
664
  }
650
665
  this.options = {
651
- apiKey: opts.apiKey,
666
+ apiKey: credential,
652
667
  workspaceSlug: opts.workspaceSlug,
653
668
  baseUrl: (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
654
669
  fetch: f,
655
- timeoutMs: opts.timeoutMs ?? 6e4
670
+ timeoutMs: opts.timeoutMs ?? 6e4,
671
+ tokenSource
656
672
  };
657
673
  }
658
674
  // -------------------------------------------------------------- Models
@@ -788,19 +804,7 @@ var MantyxClient = class {
788
804
  let lastSeq = 0;
789
805
  while (true) {
790
806
  const reqUrl = lastSeq > 0 ? `${url}?lastSeq=${lastSeq}` : url;
791
- const res = await this.options.fetch(reqUrl, {
792
- method: "GET",
793
- headers: {
794
- ...this.authHeaders(),
795
- Accept: "text/event-stream",
796
- ...lastSeq > 0 ? { "Last-Event-ID": String(lastSeq) } : {}
797
- },
798
- ...signal ? { signal } : {}
799
- }).catch((err) => {
800
- throw new MantyxNetworkError(`Failed to open SSE stream: ${err.message}`, {
801
- cause: err
802
- });
803
- });
807
+ const res = await this.openSseStream(reqUrl, lastSeq, signal);
804
808
  if (!res.ok) {
805
809
  throw await this.errorFromResponse(res);
806
810
  }
@@ -903,18 +907,72 @@ var MantyxClient = class {
903
907
  absoluteUrl(path) {
904
908
  return `${this.options.baseUrl}/api/v1/workspaces/${encodeURIComponent(this.options.workspaceSlug)}${path}`;
905
909
  }
906
- authHeaders() {
907
- return { Authorization: `Bearer ${this.options.apiKey}` };
910
+ /**
911
+ * Resolve the bearer credential to send on the next request. With a
912
+ * static `apiKey` / `accessToken` this is a synchronous reach into
913
+ * `options.apiKey`; with a {@link TokenSource} it delegates so the
914
+ * source can refresh expired access tokens before we hit the wire.
915
+ *
916
+ * The `reason` is forwarded to the source verbatim. Pass
917
+ * `"unauthorized"` immediately after a 401 so the source forces a
918
+ * refresh rather than handing back its (now-invalid) cached value.
919
+ */
920
+ async resolveBearer(reason = "initial") {
921
+ if (this.options.tokenSource) return this.options.tokenSource(reason);
922
+ return this.options.apiKey;
923
+ }
924
+ /**
925
+ * Open an SSE stream against `reqUrl` with at-most-one refresh +
926
+ * retry on 401. The caller is responsible for the subsequent
927
+ * `readSseStream` loop; this helper only handles the initial GET.
928
+ * Mid-stream 401s propagate as `MantyxNetworkError` from the read
929
+ * loop and trigger a reconnect via the outer `while` in
930
+ * {@link streamRunEvents}.
931
+ */
932
+ async openSseStream(reqUrl, lastSeq, signal) {
933
+ const openOnce = async (reason) => {
934
+ const auth = await this.authHeaders(reason);
935
+ return this.options.fetch(reqUrl, {
936
+ method: "GET",
937
+ headers: {
938
+ ...auth,
939
+ Accept: "text/event-stream",
940
+ ...lastSeq > 0 ? { "Last-Event-ID": String(lastSeq) } : {}
941
+ },
942
+ ...signal ? { signal } : {}
943
+ }).catch((err) => {
944
+ throw new MantyxNetworkError(`Failed to open SSE stream: ${err.message}`, {
945
+ cause: err
946
+ });
947
+ });
948
+ };
949
+ const res = await openOnce("initial");
950
+ if (res.status === 401 && this.options.tokenSource !== null) {
951
+ try {
952
+ await res.text();
953
+ } catch {
954
+ }
955
+ return openOnce("unauthorized");
956
+ }
957
+ return res;
958
+ }
959
+ async authHeaders(reason = "initial") {
960
+ const bearer = await this.resolveBearer(reason);
961
+ return { Authorization: `Bearer ${bearer}` };
908
962
  }
909
963
  async request(args) {
964
+ return this.requestWithRetry(args, "initial");
965
+ }
966
+ async requestWithRetry(args, reason) {
910
967
  const url = this.absoluteUrl(args.path);
911
968
  const ctrl = new AbortController();
912
969
  const t = setTimeout(() => ctrl.abort(), args.timeoutMs ?? this.options.timeoutMs);
913
970
  try {
971
+ const auth = await this.authHeaders(reason);
914
972
  const res = await this.options.fetch(url, {
915
973
  method: args.method,
916
974
  headers: {
917
- ...this.authHeaders(),
975
+ ...auth,
918
976
  ...args.body !== void 0 ? { "Content-Type": "application/json" } : {},
919
977
  Accept: "application/json"
920
978
  },
@@ -927,6 +985,14 @@ var MantyxClient = class {
927
985
  throw new MantyxNetworkError(`Network error: ${err.message}`, { cause: err });
928
986
  });
929
987
  if (!res.ok) {
988
+ if (res.status === 401 && this.options.tokenSource !== null && reason === "initial") {
989
+ try {
990
+ await res.text();
991
+ } catch {
992
+ }
993
+ clearTimeout(t);
994
+ return this.requestWithRetry(args, "unauthorized");
995
+ }
930
996
  throw await this.errorFromResponse(res);
931
997
  }
932
998
  const text = await res.text();
@@ -947,7 +1013,12 @@ var MantyxClient = class {
947
1013
  } catch {
948
1014
  }
949
1015
  if (res.status === 401) {
950
- return new MantyxAuthError(body.error ?? "Invalid API key");
1016
+ return new MantyxAuthError(body.error ?? "Invalid API key or OAuth access token");
1017
+ }
1018
+ if (res.status === 403 && (body.error === "insufficient_scope" || body.code === "insufficient_scope")) {
1019
+ const required = parseRequiredScopes(body.required, res.headers.get("WWW-Authenticate"));
1020
+ const msg = required.length > 0 ? `Missing OAuth scope${required.length > 1 ? "s" : ""}: ${required.join(", ")}` : "OAuth access token is missing a required scope";
1021
+ return new MantyxScopeError(msg, required);
951
1022
  }
952
1023
  return new MantyxError(body.error ?? `HTTP ${res.status}`, {
953
1024
  code: body.code ?? `http_${res.status}`,
@@ -1313,19 +1384,278 @@ function parseRunOutput(result, validator) {
1313
1384
  function sleep(ms) {
1314
1385
  return new Promise((r) => setTimeout(r, ms));
1315
1386
  }
1387
+ function resolveCredential(opts) {
1388
+ const apiKey = typeof opts.apiKey === "string" ? opts.apiKey : "";
1389
+ const accessToken = typeof opts.accessToken === "string" ? opts.accessToken : "";
1390
+ const tokenSource = typeof opts.tokenSource === "function" ? opts.tokenSource : null;
1391
+ const provided = [apiKey ? "apiKey" : "", accessToken ? "accessToken" : "", tokenSource ? "tokenSource" : ""].filter((s) => s.length > 0);
1392
+ if (provided.length > 1) {
1393
+ throw new MantyxError(
1394
+ `Pass exactly one of \`apiKey\`, \`accessToken\`, or \`tokenSource\` \u2014 got ${provided.join(" + ")}.`
1395
+ );
1396
+ }
1397
+ if (provided.length === 0) {
1398
+ throw new MantyxError(
1399
+ "One of `apiKey` (workspace API key), `accessToken` (OAuth access token), or `tokenSource` (dynamic credential provider) is required"
1400
+ );
1401
+ }
1402
+ return {
1403
+ credential: apiKey || accessToken,
1404
+ tokenSource
1405
+ };
1406
+ }
1407
+ function parseRequiredScopes(bodyRequired, wwwAuthenticate) {
1408
+ if (Array.isArray(bodyRequired)) {
1409
+ return bodyRequired.filter((s) => typeof s === "string" && s.length > 0);
1410
+ }
1411
+ if (typeof bodyRequired === "string" && bodyRequired.length > 0) {
1412
+ return [bodyRequired];
1413
+ }
1414
+ if (typeof wwwAuthenticate === "string") {
1415
+ const m = /scope="([^"]+)"/i.exec(wwwAuthenticate);
1416
+ if (m && m[1]) {
1417
+ return m[1].split(/\s+/).filter((s) => s.length > 0);
1418
+ }
1419
+ }
1420
+ return [];
1421
+ }
1422
+
1423
+ // src/oauth.ts
1424
+ var DEFAULT_OAUTH_BASE_URL = "https://app.mantyx.io";
1425
+ var DEFAULT_REFRESH_SKEW_MS = 6e4;
1426
+ var MantyxOAuthError = class extends MantyxError {
1427
+ oauthError;
1428
+ oauthErrorDescription;
1429
+ constructor(oauthError, oauthErrorDescription, status) {
1430
+ const message = oauthErrorDescription ? `OAuth ${oauthError}: ${oauthErrorDescription}` : `OAuth ${oauthError}`;
1431
+ super(message, { code: oauthError, status });
1432
+ this.name = "MantyxOAuthError";
1433
+ this.oauthError = oauthError;
1434
+ this.oauthErrorDescription = oauthErrorDescription;
1435
+ }
1436
+ };
1437
+ var MantyxOAuthClient = class {
1438
+ clientId;
1439
+ baseUrl;
1440
+ clientSecret;
1441
+ fetchImpl;
1442
+ timeoutMs;
1443
+ constructor(opts) {
1444
+ if (!opts.clientId) {
1445
+ throw new MantyxError("`clientId` is required for MantyxOAuthClient");
1446
+ }
1447
+ if (!opts.clientSecret) {
1448
+ throw new MantyxError("`clientSecret` is required for MantyxOAuthClient");
1449
+ }
1450
+ const f = opts.fetch ?? globalThis.fetch;
1451
+ if (typeof f !== "function") {
1452
+ throw new MantyxError(
1453
+ "Global fetch is not available; pass a custom `fetch` implementation in MantyxOAuthClientOptions."
1454
+ );
1455
+ }
1456
+ this.clientId = opts.clientId;
1457
+ this.clientSecret = opts.clientSecret;
1458
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_OAUTH_BASE_URL).replace(/\/+$/, "");
1459
+ this.fetchImpl = f;
1460
+ this.timeoutMs = opts.timeoutMs ?? 3e4;
1461
+ }
1462
+ /**
1463
+ * Mint a fresh access token from a stored refresh token. The
1464
+ * returned `refreshToken` is identical to the input — refresh
1465
+ * tokens are persistent and non-rotating, so the field is
1466
+ * surfaced only for symmetry with the response shape.
1467
+ *
1468
+ * On `400 invalid_grant` the refresh token has been revoked (or its
1469
+ * grant / app was deleted); the SDK surfaces a
1470
+ * {@link MantyxOAuthError} and callers must drive a fresh sign-in.
1471
+ */
1472
+ async refresh(opts) {
1473
+ if (!opts.refreshToken) {
1474
+ throw new MantyxError("`refreshToken` is required for MantyxOAuthClient.refresh");
1475
+ }
1476
+ const body = {
1477
+ grant_type: "refresh_token",
1478
+ refresh_token: opts.refreshToken
1479
+ };
1480
+ const scope = normalizeScope(opts.scope);
1481
+ if (scope !== void 0) body.scope = scope;
1482
+ return this.token(body);
1483
+ }
1484
+ /**
1485
+ * Revoke an access or refresh token (RFC 7009). The server always
1486
+ * returns 200, even for unknown tokens. Revoking a **refresh**
1487
+ * token kills the refresh and every live access token tied to its
1488
+ * grant; revoking an **access** token kills only that one.
1489
+ */
1490
+ async revoke(opts) {
1491
+ if (!opts.token) {
1492
+ throw new MantyxError("`token` is required for MantyxOAuthClient.revoke");
1493
+ }
1494
+ await this.formPost("/api/oauth/revoke", {
1495
+ token: opts.token
1496
+ });
1497
+ }
1498
+ /**
1499
+ * Build a long-lived {@link TokenSource} that re-mints access
1500
+ * tokens from the supplied refresh token. Pass the returned source
1501
+ * to `new MantyxClient({ tokenSource, workspaceSlug, ... })`. The
1502
+ * source caches the access token in-memory and refreshes
1503
+ * proactively when the cached value is within `refreshSkewMs` of
1504
+ * `expiresAt`, or eagerly when `MantyxClient` reports a 401.
1505
+ *
1506
+ * Pass `initialToken` if the calling app already has a non-expired
1507
+ * access token in hand (e.g. straight out of the sign-in flow) to
1508
+ * avoid an extra round-trip on the first request.
1509
+ */
1510
+ refreshTokenSource(opts) {
1511
+ if (!opts.refreshToken) {
1512
+ throw new MantyxError("`refreshToken` is required for MantyxOAuthClient.refreshTokenSource");
1513
+ }
1514
+ const skew = opts.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;
1515
+ const cache = { token: opts.initialToken, inflight: null };
1516
+ const refreshToken = opts.refreshToken;
1517
+ return makeTokenSource(cache, skew, async () => {
1518
+ return this.refresh({ refreshToken, scope: opts.scope });
1519
+ });
1520
+ }
1521
+ // -------------------------------------------------------------- internals
1522
+ /**
1523
+ * POST `application/x-www-form-urlencoded` to `/api/oauth/token` and
1524
+ * decode the {@link OAuthToken} response. Always injects `client_id`
1525
+ * + `client_secret` from the constructor.
1526
+ */
1527
+ async token(body) {
1528
+ const res = await this.formPost("/api/oauth/token", body);
1529
+ let parsed = {};
1530
+ try {
1531
+ parsed = await res.json();
1532
+ } catch {
1533
+ throw new MantyxOAuthError(
1534
+ "invalid_response",
1535
+ "Token endpoint returned a non-JSON response",
1536
+ res.status
1537
+ );
1538
+ }
1539
+ const accessToken = typeof parsed.access_token === "string" ? parsed.access_token : "";
1540
+ if (!accessToken) {
1541
+ throw new MantyxOAuthError(
1542
+ "invalid_response",
1543
+ "Token endpoint response is missing `access_token`",
1544
+ res.status
1545
+ );
1546
+ }
1547
+ const expiresIn = typeof parsed.expires_in === "number" ? parsed.expires_in : 3600;
1548
+ return {
1549
+ accessToken,
1550
+ refreshToken: typeof parsed.refresh_token === "string" ? parsed.refresh_token : void 0,
1551
+ tokenType: typeof parsed.token_type === "string" ? parsed.token_type : "Bearer",
1552
+ expiresIn,
1553
+ expiresAt: Date.now() + expiresIn * 1e3,
1554
+ scope: typeof parsed.scope === "string" ? parsed.scope : void 0
1555
+ };
1556
+ }
1557
+ async formPost(path, body) {
1558
+ const url = `${this.baseUrl}${path}`;
1559
+ const params = new URLSearchParams({
1560
+ ...body,
1561
+ client_id: this.clientId,
1562
+ client_secret: this.clientSecret
1563
+ });
1564
+ const ctrl = new AbortController();
1565
+ const t = setTimeout(() => ctrl.abort(), this.timeoutMs);
1566
+ let res;
1567
+ try {
1568
+ res = await this.fetchImpl(url, {
1569
+ method: "POST",
1570
+ headers: {
1571
+ "Content-Type": "application/x-www-form-urlencoded",
1572
+ Accept: "application/json"
1573
+ },
1574
+ body: params.toString(),
1575
+ signal: ctrl.signal
1576
+ });
1577
+ } catch (err) {
1578
+ if (ctrl.signal.aborted) {
1579
+ throw new MantyxNetworkError(`OAuth request timed out after ${this.timeoutMs}ms`);
1580
+ }
1581
+ throw new MantyxNetworkError(`OAuth network error: ${err.message}`, {
1582
+ cause: err
1583
+ });
1584
+ } finally {
1585
+ clearTimeout(t);
1586
+ }
1587
+ if (!res.ok) {
1588
+ let errBody = {};
1589
+ try {
1590
+ errBody = await res.json();
1591
+ } catch {
1592
+ }
1593
+ const oauthError = typeof errBody.error === "string" ? errBody.error : `http_${res.status}`;
1594
+ const desc = typeof errBody.error_description === "string" ? errBody.error_description : void 0;
1595
+ throw new MantyxOAuthError(oauthError, desc, res.status);
1596
+ }
1597
+ return res;
1598
+ }
1599
+ };
1600
+ function makeTokenSource(cache, skewMs, mint) {
1601
+ return async (reason = "initial") => {
1602
+ if (reason !== "unauthorized" && cache.token && !isExpiring(cache.token, skewMs)) {
1603
+ return cache.token.accessToken;
1604
+ }
1605
+ if (cache.inflight) {
1606
+ const t = await cache.inflight;
1607
+ if (reason === "unauthorized" && t === cache.token) {
1608
+ } else {
1609
+ return t.accessToken;
1610
+ }
1611
+ }
1612
+ cache.inflight = mint().then(
1613
+ (t) => {
1614
+ cache.token = t;
1615
+ return t;
1616
+ },
1617
+ (err) => {
1618
+ throw err;
1619
+ }
1620
+ );
1621
+ try {
1622
+ const t = await cache.inflight;
1623
+ return t.accessToken;
1624
+ } finally {
1625
+ cache.inflight = null;
1626
+ }
1627
+ };
1628
+ }
1629
+ function isExpiring(token, skewMs) {
1630
+ return token.expiresAt - Date.now() <= skewMs;
1631
+ }
1632
+ function normalizeScope(scope) {
1633
+ if (scope === void 0) return void 0;
1634
+ if (typeof scope === "string") {
1635
+ const trimmed = scope.trim();
1636
+ return trimmed.length > 0 ? trimmed : void 0;
1637
+ }
1638
+ const joined = scope.filter((s) => typeof s === "string" && s.length > 0).join(" ");
1639
+ return joined.length > 0 ? joined : void 0;
1640
+ }
1316
1641
 
1317
1642
  // src/version.ts
1318
- var SDK_VERSION = "0.9.1";
1643
+ var SDK_VERSION = "0.10.1";
1319
1644
  // Annotate the CommonJS export names for ESM import in node:
1320
1645
  0 && (module.exports = {
1321
1646
  AgentSession,
1322
1647
  DEFAULT_BASE_URL,
1648
+ DEFAULT_OAUTH_BASE_URL,
1649
+ DEFAULT_REFRESH_SKEW_MS,
1323
1650
  MantyxAuthError,
1324
1651
  MantyxClient,
1325
1652
  MantyxError,
1326
1653
  MantyxNetworkError,
1654
+ MantyxOAuthClient,
1655
+ MantyxOAuthError,
1327
1656
  MantyxParseError,
1328
1657
  MantyxRunError,
1658
+ MantyxScopeError,
1329
1659
  MantyxToolError,
1330
1660
  SDK_VERSION,
1331
1661
  defineLocalA2A,