@kontext-dev/js-sdk 0.3.0 → 1.1.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.
Files changed (63) hide show
  1. package/dist/adapters/ai/index.cjs +12 -2
  2. package/dist/adapters/ai/index.cjs.map +1 -1
  3. package/dist/adapters/ai/index.js +12 -2
  4. package/dist/adapters/ai/index.js.map +1 -1
  5. package/dist/adapters/cloudflare/index.cjs +13 -0
  6. package/dist/adapters/cloudflare/index.cjs.map +1 -1
  7. package/dist/adapters/cloudflare/index.js +13 -0
  8. package/dist/adapters/cloudflare/index.js.map +1 -1
  9. package/dist/adapters/cloudflare/react.cjs +12 -2
  10. package/dist/adapters/cloudflare/react.cjs.map +1 -1
  11. package/dist/adapters/cloudflare/react.js +12 -2
  12. package/dist/adapters/cloudflare/react.js.map +1 -1
  13. package/dist/adapters/react/index.cjs +12 -2
  14. package/dist/adapters/react/index.cjs.map +1 -1
  15. package/dist/adapters/react/index.js +12 -2
  16. package/dist/adapters/react/index.js.map +1 -1
  17. package/dist/client/index.cjs +108 -69
  18. package/dist/client/index.cjs.map +1 -1
  19. package/dist/client/index.d.cts +2 -0
  20. package/dist/client/index.d.ts +2 -0
  21. package/dist/client/index.js +109 -71
  22. package/dist/client/index.js.map +1 -1
  23. package/dist/errors.cjs +78 -0
  24. package/dist/errors.cjs.map +1 -1
  25. package/dist/errors.d.cts +7 -1
  26. package/dist/errors.d.ts +7 -1
  27. package/dist/errors.js +78 -1
  28. package/dist/errors.js.map +1 -1
  29. package/dist/index.cjs +151 -87
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +152 -88
  34. package/dist/index.js.map +1 -1
  35. package/dist/{kontext-CgIBANFo.d.cts → kontext-CBPuE-hq.d.cts} +3 -0
  36. package/dist/{kontext-CgIBANFo.d.ts → kontext-CBPuE-hq.d.ts} +3 -0
  37. package/dist/management/index.cjs +15 -0
  38. package/dist/management/index.cjs.map +1 -1
  39. package/dist/management/index.d.cts +2 -2
  40. package/dist/management/index.d.ts +2 -2
  41. package/dist/management/index.js +15 -1
  42. package/dist/management/index.js.map +1 -1
  43. package/dist/mcp/index.cjs +32 -3
  44. package/dist/mcp/index.cjs.map +1 -1
  45. package/dist/mcp/index.d.cts +7 -1
  46. package/dist/mcp/index.d.ts +7 -1
  47. package/dist/mcp/index.js +33 -4
  48. package/dist/mcp/index.js.map +1 -1
  49. package/dist/oauth/index.cjs +12 -2
  50. package/dist/oauth/index.cjs.map +1 -1
  51. package/dist/oauth/index.d.cts +1 -1
  52. package/dist/oauth/index.d.ts +1 -1
  53. package/dist/oauth/index.js +12 -2
  54. package/dist/oauth/index.js.map +1 -1
  55. package/dist/server/index.cjs +55 -20
  56. package/dist/server/index.cjs.map +1 -1
  57. package/dist/server/index.d.cts +2 -2
  58. package/dist/server/index.d.ts +2 -2
  59. package/dist/server/index.js +56 -21
  60. package/dist/server/index.js.map +1 -1
  61. package/dist/{types-CzhnlJHW.d.cts → types-DicGI7ix.d.cts} +23 -1
  62. package/dist/{types-CzhnlJHW.d.ts → types-DicGI7ix.d.ts} +23 -1
  63. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { ClientState, ConnectSessionResult, IntegrationInfo, KontextClient, KontextClientConfig, KontextTool, ToolResult, createKontextClient } from './client/index.cjs';
2
2
  export { K as KontextOrchestrator, a as KontextOrchestratorConfig, b as KontextOrchestratorState, c as createKontextOrchestrator } from './index-DcL4a5Vq.cjs';
3
- export { I as IntegrationCredential, a as IntegrationName, b as IntegrationResolvedCredentials, K as KnownIntegration, c as Kontext, d as KontextOptions, M as McpServerFactory, e as McpServerOrFactory, f as MiddlewareOptions } from './kontext-CgIBANFo.cjs';
3
+ export { I as IntegrationCredential, a as IntegrationName, b as IntegrationResolvedCredentials, K as KnownIntegration, c as Kontext, d as KontextOptions, M as McpServerFactory, e as McpServerOrFactory, f as MiddlewareOptions } from './kontext-CBPuE-hq.cjs';
4
4
  export { K as KontextTokenVerifier, a as KontextTokenVerifierConfig } from './verifier-CoJmYiw3.cjs';
5
5
  export { AuthorizationRequiredError, ConfigError, ElicitationEntry, HttpError, IntegrationConnectionRequiredError, KontextError, NetworkError, OAuthError, isKontextError, isNetworkError, isUnauthorizedError, parseHttpError } from './errors.cjs';
6
6
  import './mcp/index.cjs';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { ClientState, ConnectSessionResult, IntegrationInfo, KontextClient, KontextClientConfig, KontextTool, ToolResult, createKontextClient } from './client/index.js';
2
2
  export { K as KontextOrchestrator, a as KontextOrchestratorConfig, b as KontextOrchestratorState, c as createKontextOrchestrator } from './index-D5hS5PGn.js';
3
- export { I as IntegrationCredential, a as IntegrationName, b as IntegrationResolvedCredentials, K as KnownIntegration, c as Kontext, d as KontextOptions, M as McpServerFactory, e as McpServerOrFactory, f as MiddlewareOptions } from './kontext-CgIBANFo.js';
3
+ export { I as IntegrationCredential, a as IntegrationName, b as IntegrationResolvedCredentials, K as KnownIntegration, c as Kontext, d as KontextOptions, M as McpServerFactory, e as McpServerOrFactory, f as MiddlewareOptions } from './kontext-CBPuE-hq.js';
4
4
  export { K as KontextTokenVerifier, a as KontextTokenVerifierConfig } from './verifier-CoJmYiw3.js';
5
5
  export { AuthorizationRequiredError, ConfigError, ElicitationEntry, HttpError, IntegrationConnectionRequiredError, KontextError, NetworkError, OAuthError, isKontextError, isNetworkError, isUnauthorizedError, parseHttpError } from './errors.js';
6
6
  import './mcp/index.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
- import { isInitializeRequest, UrlElicitationRequiredError, ElicitRequestSchema, ElicitationCompleteNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
3
+ import { ErrorCode, isInitializeRequest, UrlElicitationRequiredError, ElicitRequestSchema, ElicitationCompleteNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { createHash, randomBytes } from 'crypto';
5
5
  import { createRequire } from 'module';
6
6
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -73,8 +73,6 @@ var StorageKeys = {
73
73
  function resourceTokenKey(resource) {
74
74
  return `${StorageKeys.RESOURCE_TOKENS}:${resource}`;
75
75
  }
76
-
77
- // src/errors.ts
78
76
  var KontextError = class extends Error {
79
77
  /** Brand field for type narrowing without instanceof */
80
78
  kontextError = true;
@@ -207,12 +205,20 @@ function isNetworkError(err) {
207
205
  if (typeof causeCode === "string" && NETWORK_ERROR_CODES.has(causeCode))
208
206
  return true;
209
207
  }
208
+ if (err.name === "TypeError") {
209
+ const msg = err.message.toLowerCase();
210
+ if (msg === "failed to fetch" || msg === "load failed" || msg.includes("networkerror")) {
211
+ return true;
212
+ }
213
+ }
210
214
  return false;
211
215
  }
212
216
  function isUnauthorizedError(err) {
213
217
  const props = errorProps(err);
214
218
  if (props.statusCode === 401 || props.status === 401) return true;
219
+ if (props.code === 401) return true;
215
220
  if (err.name === "UnauthorizedError") return true;
221
+ if (err.constructor?.name === "UnauthorizedError") return true;
216
222
  if (err.message === "Unauthorized") return true;
217
223
  return false;
218
224
  }
@@ -271,6 +277,73 @@ function parseHttpError(statusCode, body) {
271
277
  });
272
278
  }
273
279
  }
280
+ var MCP_CODE_MAP = {
281
+ [ErrorCode.ParseError]: { code: "kontext_mcp_parse_error" },
282
+ [ErrorCode.InvalidRequest]: { code: "kontext_mcp_invalid_request" },
283
+ [ErrorCode.MethodNotFound]: { code: "kontext_mcp_method_not_found" },
284
+ [ErrorCode.InvalidParams]: { code: "kontext_mcp_invalid_params" },
285
+ [ErrorCode.InternalError]: {
286
+ code: "kontext_mcp_internal_error",
287
+ statusCode: 500
288
+ },
289
+ [ErrorCode.RequestTimeout]: {
290
+ code: "kontext_mcp_session_expired",
291
+ statusCode: 401
292
+ },
293
+ [ErrorCode.ConnectionClosed]: { code: "kontext_mcp_session_error" }
294
+ };
295
+ function translateError(err) {
296
+ if (isKontextError(err)) return err;
297
+ if (!(err instanceof Error)) {
298
+ return new KontextError(String(err), "kontext_unknown_error");
299
+ }
300
+ const props = err;
301
+ if (props.code === ErrorCode.UrlElicitationRequired) {
302
+ const elicitations = props.elicitations ?? props.data?.elicitations;
303
+ const elicitation = elicitations?.[0];
304
+ return new IntegrationConnectionRequiredError(
305
+ elicitation?.integrationId ?? "unknown",
306
+ {
307
+ integrationName: elicitation?.integrationName,
308
+ connectUrl: elicitation?.url,
309
+ message: elicitation?.message,
310
+ cause: err
311
+ }
312
+ );
313
+ }
314
+ if (typeof props.code === "number" && props.code < 0) {
315
+ const entry = MCP_CODE_MAP[props.code];
316
+ if (entry) {
317
+ return new KontextError(err.message, entry.code, {
318
+ statusCode: entry.statusCode,
319
+ cause: err
320
+ });
321
+ }
322
+ return new KontextError(err.message, "kontext_mcp_error", {
323
+ cause: err,
324
+ meta: { mcpCode: props.code }
325
+ });
326
+ }
327
+ const statusCode = props.statusCode ?? props.status ?? (typeof props.code === "number" && props.code >= 400 && props.code < 600 ? props.code : void 0);
328
+ if (typeof statusCode === "number" && statusCode >= 400) {
329
+ if (statusCode === 401) {
330
+ return new AuthorizationRequiredError(err.message, { cause: err });
331
+ }
332
+ return new KontextError(err.message, "kontext_server_error", {
333
+ statusCode,
334
+ cause: err
335
+ });
336
+ }
337
+ if (isUnauthorizedError(err)) {
338
+ return new AuthorizationRequiredError(err.message, { cause: err });
339
+ }
340
+ if (isNetworkError(err)) {
341
+ return new NetworkError(err.message, { cause: err });
342
+ }
343
+ return new KontextError(err.message, "kontext_unknown_error", {
344
+ cause: err
345
+ });
346
+ }
274
347
 
275
348
  // src/oauth/provider.ts
276
349
  var KontextOAuthProvider = class {
@@ -708,7 +781,22 @@ var KontextMcp = class {
708
781
  const url = typeof item.url === "string" ? item.url : "";
709
782
  if (!id || !url) return null;
710
783
  const category = item.category === "internal_mcp_credentials" ? "internal_mcp_credentials" : "gateway_remote_mcp";
711
- const connectType = item.connectType === "credentials" || item.connectType === "oauth" || item.connectType === "none" ? item.connectType : category === "internal_mcp_credentials" ? "credentials" : item.authMode === "oauth" ? "oauth" : "none";
784
+ const rawConnectType = item.connectType;
785
+ if (typeof rawConnectType !== "string") {
786
+ throw new KontextError(
787
+ "Runtime integration connectType is required in API response.",
788
+ "kontext_runtime_integrations_invalid_response",
789
+ { meta: { integrationId: id, connectType: rawConnectType } }
790
+ );
791
+ }
792
+ const connectType = rawConnectType === "credentials" || rawConnectType === "oauth" || rawConnectType === "user_token" || rawConnectType === "none" ? rawConnectType : null;
793
+ if (!connectType) {
794
+ throw new KontextError(
795
+ `Unknown runtime integration connectType "${rawConnectType}".`,
796
+ "kontext_runtime_integrations_invalid_response",
797
+ { meta: { integrationId: id, connectType: rawConnectType } }
798
+ );
799
+ }
712
800
  const rawConnection = item.connection && typeof item.connection === "object" ? item.connection : void 0;
713
801
  const connected = rawConnection && typeof rawConnection.connected === "boolean" ? rawConnection.connected : false;
714
802
  const status = rawConnection?.status === "connected" ? "connected" : "disconnected";
@@ -719,6 +807,9 @@ var KontextMcp = class {
719
807
  category,
720
808
  connectType,
721
809
  authMode: item.authMode === "oauth" || item.authMode === "user_token" || item.authMode === "server_token" || item.authMode === "none" ? item.authMode : void 0,
810
+ tokenLabel: typeof item.tokenLabel === "string" ? item.tokenLabel : void 0,
811
+ tokenHelpUrl: typeof item.tokenHelpUrl === "string" ? item.tokenHelpUrl : void 0,
812
+ tokenPlaceholder: typeof item.tokenPlaceholder === "string" ? item.tokenPlaceholder : void 0,
722
813
  credentialSchema: item.credentialSchema,
723
814
  requiresOauth: typeof item.requiresOauth === "boolean" ? item.requiresOauth : void 0,
724
815
  connection: rawConnection ? {
@@ -1420,35 +1511,21 @@ async function withTransientRetry(operation, maxRetries = 1) {
1420
1511
  function toKontextError(err, context) {
1421
1512
  const contextMeta = context ? { ...context } : void 0;
1422
1513
  const mergeMeta = (base) => contextMeta ? { ...base ?? {}, ...contextMeta } : base ?? {};
1423
- if (isKontextError(err)) {
1424
- if (!contextMeta) {
1425
- return err;
1426
- }
1427
- const cloned = Object.create(Object.getPrototypeOf(err));
1428
- Object.defineProperties(cloned, Object.getOwnPropertyDescriptors(err));
1429
- Object.defineProperty(cloned, "meta", {
1430
- value: mergeMeta(err.meta),
1431
- enumerable: true,
1432
- writable: true,
1433
- configurable: true
1434
- });
1435
- return cloned;
1436
- }
1437
- if (err instanceof Error) {
1438
- if (isUnauthorizedError(err)) {
1439
- return new AuthorizationRequiredError(err.message, {
1440
- meta: mergeMeta(),
1441
- cause: err
1442
- });
1443
- }
1444
- return new KontextError(err.message, "kontext_unknown_error", {
1445
- meta: mergeMeta(),
1446
- cause: err
1447
- });
1514
+ const translated = translateError(err);
1515
+ if (!contextMeta) {
1516
+ return translated;
1448
1517
  }
1449
- return new KontextError(String(err), "kontext_unknown_error", {
1450
- meta: mergeMeta()
1518
+ const cloned = Object.create(
1519
+ Object.getPrototypeOf(translated)
1520
+ );
1521
+ Object.defineProperties(cloned, Object.getOwnPropertyDescriptors(translated));
1522
+ Object.defineProperty(cloned, "meta", {
1523
+ value: mergeMeta(translated.meta),
1524
+ enumerable: true,
1525
+ writable: true,
1526
+ configurable: true
1451
1527
  });
1528
+ return cloned;
1452
1529
  }
1453
1530
  function isAuthorizationRequired(err) {
1454
1531
  if (err instanceof AuthorizationRequiredError) return true;
@@ -2162,45 +2239,6 @@ function extractTextContent(result) {
2162
2239
  }
2163
2240
  return JSON.stringify(result);
2164
2241
  }
2165
- function translateError(err) {
2166
- if (isKontextError(err)) return err;
2167
- if (!(err instanceof Error)) {
2168
- return new KontextError(String(err), "kontext_unknown_error");
2169
- }
2170
- const props = err;
2171
- if (props.code === -32042) {
2172
- const elicitations = props.elicitations ?? props.data?.elicitations;
2173
- const elicitation = elicitations?.[0];
2174
- return new IntegrationConnectionRequiredError(
2175
- elicitation?.integrationId ?? "unknown",
2176
- {
2177
- integrationName: elicitation?.integrationName,
2178
- connectUrl: elicitation?.url,
2179
- message: elicitation?.message,
2180
- cause: err
2181
- }
2182
- );
2183
- }
2184
- const statusCode = props.statusCode ?? props.status;
2185
- if (typeof statusCode === "number" && statusCode >= 400) {
2186
- if (statusCode === 401) {
2187
- return new AuthorizationRequiredError(err.message, { cause: err });
2188
- }
2189
- return new KontextError(err.message, "kontext_server_error", {
2190
- statusCode,
2191
- cause: err
2192
- });
2193
- }
2194
- if (isUnauthorizedError(err)) {
2195
- return new AuthorizationRequiredError(err.message, { cause: err });
2196
- }
2197
- if (isNetworkError(err)) {
2198
- return new NetworkError(err.message, { cause: err });
2199
- }
2200
- return new KontextError(err.message, "kontext_unknown_error", {
2201
- cause: err
2202
- });
2203
- }
2204
2242
  function createSingleEndpointKontextClient(config) {
2205
2243
  if (!config.clientId) {
2206
2244
  throw new ConfigError(
@@ -2490,6 +2528,7 @@ function createKontextClient(config) {
2490
2528
  // src/management/types.ts
2491
2529
  var TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
2492
2530
  var TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";
2531
+ var TOKEN_TYPE_USER_ID = "urn:kontext:user-id";
2493
2532
 
2494
2533
  // src/oauth/token-exchange.ts
2495
2534
  async function exchangeToken(config, subjectToken, resource, scope, subjectTokenType = TOKEN_TYPE_ACCESS_TOKEN) {
@@ -2969,7 +3008,7 @@ var Kontext = class _Kontext {
2969
3008
  oauthMetadata = null;
2970
3009
  metadataFetchedAt = 0;
2971
3010
  metadataPromise = null;
2972
- // Token exchange caching: keyed by `${integration}\0${subjectToken}`
3011
+ // Token exchange caching: keyed by `${mode}\0${integration}\0${subjectToken}`
2973
3012
  credentialCache = /* @__PURE__ */ new Map();
2974
3013
  resolvedCredentialCache = /* @__PURE__ */ new Map();
2975
3014
  runtimeAuthCache = /* @__PURE__ */ new Map();
@@ -3127,23 +3166,28 @@ var Kontext = class _Kontext {
3127
3166
  router.delete(mcpPath, mcpHandler.delete);
3128
3167
  return router;
3129
3168
  }
3130
- // ===========================================================================
3131
- // require()
3132
- // ===========================================================================
3133
- /**
3134
- * Exchange a user's access token for an integration credential.
3135
- *
3136
- * @param integration - Integration name (e.g., "github")
3137
- * @param token - The user's Bearer token (from `authInfo.token`)
3138
- * @returns Integration credential with `accessToken` and `authorization` header
3139
- *
3140
- * @throws {IntegrationConnectionRequiredError} User hasn't connected this integration
3141
- * @throws {OAuthError} Token exchange failed
3142
- */
3143
- async require(integration, token) {
3169
+ async require(integration, tokenOrOpts) {
3170
+ let isUserIdMode = false;
3171
+ let subjectToken = "";
3172
+ if (typeof tokenOrOpts === "string") {
3173
+ subjectToken = tokenOrOpts;
3174
+ } else if (tokenOrOpts !== null && typeof tokenOrOpts === "object" && typeof tokenOrOpts.userId === "string") {
3175
+ isUserIdMode = true;
3176
+ subjectToken = tokenOrOpts.userId.trim();
3177
+ if (!subjectToken) {
3178
+ throw new TypeError(
3179
+ "Kontext.require() expects a non-empty userId when called with { userId }."
3180
+ );
3181
+ }
3182
+ } else {
3183
+ throw new TypeError(
3184
+ "Kontext.require() expects a token string or { userId: string }."
3185
+ );
3186
+ }
3187
+ const subjectTokenType = isUserIdMode ? TOKEN_TYPE_USER_ID : void 0;
3144
3188
  const now = Date.now();
3145
3189
  this.evictExpiredCredentials(now);
3146
- const cacheKey = `${integration}\0${token}`;
3190
+ const cacheKey = isUserIdMode ? `u\0${integration}\0${subjectToken}` : `t\0${integration}\0${subjectToken}`;
3147
3191
  const cached = this.credentialCache.get(cacheKey);
3148
3192
  if (cached && now < cached.expiresAt) {
3149
3193
  this.credentialCache.delete(cacheKey);
@@ -3160,13 +3204,25 @@ var Kontext = class _Kontext {
3160
3204
  };
3161
3205
  let response;
3162
3206
  try {
3163
- response = await exchangeToken(exchangeConfig, token, integration);
3207
+ response = await exchangeToken(
3208
+ exchangeConfig,
3209
+ subjectToken,
3210
+ integration,
3211
+ void 0,
3212
+ subjectTokenType
3213
+ );
3164
3214
  } catch (err) {
3165
3215
  if (err instanceof OAuthError) {
3166
3216
  if (err.errorCode === "integration_required" || err.message.includes("not connected") || err.message.includes("expired") && err.message.includes("reconnect")) {
3167
3217
  const integrationId = err.meta.integrationId || integration;
3218
+ if (isUserIdMode) {
3219
+ throw new IntegrationConnectionRequiredError(integrationId, {
3220
+ integrationName: err.meta.integrationName,
3221
+ message: err.message
3222
+ });
3223
+ }
3168
3224
  const connectUrl = await this.fetchConnectUrl(
3169
- token,
3225
+ subjectToken,
3170
3226
  integrationId,
3171
3227
  exchangeConfig
3172
3228
  );
@@ -3677,6 +3733,12 @@ var Kontext = class _Kontext {
3677
3733
  // ===========================================================================
3678
3734
  createAgentSession(userToken, mcpSessionId, metadata) {
3679
3735
  if (!this.clientSecret || !userToken) return;
3736
+ if (!metadata?.authenticatedUserId) {
3737
+ console.warn(
3738
+ "[kontext:sessions] create skipped: missing authenticated user id"
3739
+ );
3740
+ return;
3741
+ }
3680
3742
  const tokenIdentifier = createHash("sha256").update(userToken).digest("hex");
3681
3743
  this.getServiceToken().then(
3682
3744
  (token) => fetch(`${this.apiUrl}/api/v1/agent-sessions`, {
@@ -3687,6 +3749,7 @@ var Kontext = class _Kontext {
3687
3749
  },
3688
3750
  body: JSON.stringify({
3689
3751
  tokenIdentifier,
3752
+ authenticatedUserId: metadata.authenticatedUserId,
3690
3753
  clientSessionId: mcpSessionId,
3691
3754
  hostname: metadata?.hostname,
3692
3755
  userAgent: metadata?.userAgent,
@@ -3908,6 +3971,7 @@ var Kontext = class _Kontext {
3908
3971
  status: "ok"
3909
3972
  });
3910
3973
  this.createAgentSession(authInfo?.token, sid, {
3974
+ authenticatedUserId: typeof authInfo?.extra?.sub === "string" ? authInfo.extra.sub : void 0,
3911
3975
  hostname: req.headers["x-forwarded-for"],
3912
3976
  userAgent: req.headers["user-agent"],
3913
3977
  tokenExpiresAt: authInfo?.expiresAt