mitra-interactions-sdk 1.0.69 → 1.0.71

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/README.md CHANGED
@@ -132,6 +132,18 @@ await loginMitra('mitra', {
132
132
 
133
133
  Quando qualquer requisição retorna **403**, o SDK tenta renovar o token automaticamente via iframe invisível (usa o cookie de sessão do provider). Se o refresh funcionar, a requisição é retentada com o novo token — transparente para o desenvolvedor.
134
134
 
135
+ **Sessão com duração limitada.** Quando o ambiente limita a duração da sessão, o token vale poucos minutos e traz a claim `authTime`. Nesse caso a SDK renova sozinha **antes** do vencimento (e ao voltar para a aba), sem esperar o 403. Se a sessão acabou de vez, a SDK chama `onSessionExpired`: limpe a sessão guardada e leve o usuário ao login. Para os demais tokens nada muda.
136
+
137
+ ```typescript
138
+ configureSdkMitra({
139
+ ...session,
140
+ onSessionExpired: () => {
141
+ localStorage.removeItem('minha_sessao');
142
+ window.location.reload();
143
+ }
144
+ });
145
+ ```
146
+
135
147
  O callback `onTokenRefresh` é chamado após renovação bem-sucedida:
136
148
 
137
149
  ```typescript
package/dist/index.d.mts CHANGED
@@ -417,6 +417,19 @@ interface CallIntegrationOptions {
417
417
  params?: Record<string, unknown>;
418
418
  /** Body da requisição (para POST/PUT) */
419
419
  body?: Record<string, unknown>;
420
+ /**
421
+ * Headers só desta chamada. Use apenas quando a API externa exigir um header específico.
422
+ *
423
+ * Se a API exigir corpo `application/x-www-form-urlencoded` (comum em endpoints OAuth como
424
+ * introspect, revoke e token), passe `{ 'Content-Type': 'application/x-www-form-urlencoded' }`
425
+ * e monte o `body` como objeto plano de campos texto, número ou booleano: objeto ou lista dentro
426
+ * do `body` é recusado com 400. Sem esse header, o corpo vai em JSON.
427
+ *
428
+ * A autenticação da integração continua sendo aplicada; um header com o mesmo nome de um da
429
+ * integração (ex.: `Authorization`) substitui o da integração nesta chamada. Valores são texto
430
+ * literal. Quando omitido, nenhum header é enviado.
431
+ */
432
+ headers?: Record<string, string>;
420
433
  }
421
434
  interface CallIntegrationResponse {
422
435
  /** HTTP status retornado pelo serviço externo */
@@ -1042,6 +1055,13 @@ interface MitraConfig {
1042
1055
  projectId?: number;
1043
1056
  /** Callback chamado quando o token é renovado automaticamente (após 401/403). Recebe a nova sessão. */
1044
1057
  onTokenRefresh?: (session: LoginResponse) => void;
1058
+ /**
1059
+ * Callback chamado quando a sessão acabou e não há mais como renovar: o token
1060
+ * de uma sessão com duração limitada venceu e a renovação foi recusada. O app
1061
+ * deve limpar a sessão guardada e levar o usuário ao login. Nunca é chamado
1062
+ * para token de validade longa nem em erro de permissão (403 comum).
1063
+ */
1064
+ onSessionExpired?: () => void;
1045
1065
  }
1046
1066
  /**
1047
1067
  * Configura o SDK globalmente e retorna uma instância configurada.
@@ -1102,8 +1122,15 @@ declare function loginMitra(method: 'email' | 'google' | 'microsoft' | 'mitra',
1102
1122
  /**
1103
1123
  * Renova o token silenciosamente via iframe invisível.
1104
1124
  * Se já houver um refresh em andamento, reaproveita a mesma promise.
1125
+ *
1126
+ * `currentToken` é o token que o app tem agora. Quando a página de auth pede
1127
+ * (aperto de mão `mitra-auth-ready`), ele é entregue ao iframe por postMessage.
1128
+ * Sem isso, app em domínio próprio não renova: o navegador particiona o
1129
+ * localStorage do iframe de outro site e a página de auth não acha o token que
1130
+ * gravou no login. Página de auth antiga ignora o pedido e renova pelo token
1131
+ * guardado, como sempre.
1105
1132
  */
1106
- declare function refreshTokenSilently(authUrl: string, projectId: number): Promise<LoginResponse>;
1133
+ declare function refreshTokenSilently(authUrl: string, projectId: number, currentToken?: string): Promise<LoginResponse>;
1107
1134
  /**
1108
1135
  * Cria conta via email usando iframe silencioso.
1109
1136
  * Após sucesso, o usuário recebe um código de verificação por email.
@@ -1318,6 +1345,11 @@ declare function listIntegrationsMitra(options?: ListIntegrationsOptions): Promi
1318
1345
  * POST /interactions/integrations/call
1319
1346
  * Chama uma API externa via serviço de integrações
1320
1347
  * Mapeia connection → integrationSlug no body
1348
+ *
1349
+ * `headers` é opcional e vale só para esta chamada. Com
1350
+ * `headers: { 'Content-Type': 'application/x-www-form-urlencoded' }` o `body` sai como formulário
1351
+ * (`campo=valor&campo=valor`), o que endpoints OAuth como introspect, revoke e token exigem; o
1352
+ * `body` precisa então ser um objeto plano. Sem esse header, o corpo vai em JSON.
1321
1353
  */
1322
1354
  declare function callIntegrationMitra(options: CallIntegrationOptions): Promise<CallIntegrationResponse>;
1323
1355
  /**
package/dist/index.d.ts CHANGED
@@ -417,6 +417,19 @@ interface CallIntegrationOptions {
417
417
  params?: Record<string, unknown>;
418
418
  /** Body da requisição (para POST/PUT) */
419
419
  body?: Record<string, unknown>;
420
+ /**
421
+ * Headers só desta chamada. Use apenas quando a API externa exigir um header específico.
422
+ *
423
+ * Se a API exigir corpo `application/x-www-form-urlencoded` (comum em endpoints OAuth como
424
+ * introspect, revoke e token), passe `{ 'Content-Type': 'application/x-www-form-urlencoded' }`
425
+ * e monte o `body` como objeto plano de campos texto, número ou booleano: objeto ou lista dentro
426
+ * do `body` é recusado com 400. Sem esse header, o corpo vai em JSON.
427
+ *
428
+ * A autenticação da integração continua sendo aplicada; um header com o mesmo nome de um da
429
+ * integração (ex.: `Authorization`) substitui o da integração nesta chamada. Valores são texto
430
+ * literal. Quando omitido, nenhum header é enviado.
431
+ */
432
+ headers?: Record<string, string>;
420
433
  }
421
434
  interface CallIntegrationResponse {
422
435
  /** HTTP status retornado pelo serviço externo */
@@ -1042,6 +1055,13 @@ interface MitraConfig {
1042
1055
  projectId?: number;
1043
1056
  /** Callback chamado quando o token é renovado automaticamente (após 401/403). Recebe a nova sessão. */
1044
1057
  onTokenRefresh?: (session: LoginResponse) => void;
1058
+ /**
1059
+ * Callback chamado quando a sessão acabou e não há mais como renovar: o token
1060
+ * de uma sessão com duração limitada venceu e a renovação foi recusada. O app
1061
+ * deve limpar a sessão guardada e levar o usuário ao login. Nunca é chamado
1062
+ * para token de validade longa nem em erro de permissão (403 comum).
1063
+ */
1064
+ onSessionExpired?: () => void;
1045
1065
  }
1046
1066
  /**
1047
1067
  * Configura o SDK globalmente e retorna uma instância configurada.
@@ -1102,8 +1122,15 @@ declare function loginMitra(method: 'email' | 'google' | 'microsoft' | 'mitra',
1102
1122
  /**
1103
1123
  * Renova o token silenciosamente via iframe invisível.
1104
1124
  * Se já houver um refresh em andamento, reaproveita a mesma promise.
1125
+ *
1126
+ * `currentToken` é o token que o app tem agora. Quando a página de auth pede
1127
+ * (aperto de mão `mitra-auth-ready`), ele é entregue ao iframe por postMessage.
1128
+ * Sem isso, app em domínio próprio não renova: o navegador particiona o
1129
+ * localStorage do iframe de outro site e a página de auth não acha o token que
1130
+ * gravou no login. Página de auth antiga ignora o pedido e renova pelo token
1131
+ * guardado, como sempre.
1105
1132
  */
1106
- declare function refreshTokenSilently(authUrl: string, projectId: number): Promise<LoginResponse>;
1133
+ declare function refreshTokenSilently(authUrl: string, projectId: number, currentToken?: string): Promise<LoginResponse>;
1107
1134
  /**
1108
1135
  * Cria conta via email usando iframe silencioso.
1109
1136
  * Após sucesso, o usuário recebe um código de verificação por email.
@@ -1318,6 +1345,11 @@ declare function listIntegrationsMitra(options?: ListIntegrationsOptions): Promi
1318
1345
  * POST /interactions/integrations/call
1319
1346
  * Chama uma API externa via serviço de integrações
1320
1347
  * Mapeia connection → integrationSlug no body
1348
+ *
1349
+ * `headers` é opcional e vale só para esta chamada. Com
1350
+ * `headers: { 'Content-Type': 'application/x-www-form-urlencoded' }` o `body` sai como formulário
1351
+ * (`campo=valor&campo=valor`), o que endpoints OAuth como introspect, revoke e token exigem; o
1352
+ * `body` precisa então ser um objeto plano. Sem esse header, o corpo vai em JSON.
1321
1353
  */
1322
1354
  declare function callIntegrationMitra(options: CallIntegrationOptions): Promise<CallIntegrationResponse>;
1323
1355
  /**
package/dist/index.js CHANGED
@@ -120,19 +120,28 @@ async function loginMitra(method, options) {
120
120
  return doLogin(method, options);
121
121
  }
122
122
  var REFRESH_TIMEOUT_MS = 15e3;
123
+ var AUTH_HANDOFF_READY_TYPE = "mitra-auth-ready";
124
+ var AUTH_HANDOFF_TOKEN_TYPE = "mitra-auth-refresh-token";
123
125
  var _refreshPromise = null;
124
- function openAuthIframe(url, expectedOrigin) {
126
+ function openAuthIframe(url, expectedOrigin, handoffToken) {
125
127
  return new Promise((resolve, reject) => {
126
128
  const iframe = document.createElement("iframe");
127
129
  iframe.style.display = "none";
128
130
  iframe.src = url;
129
- const timer = setTimeout(() => {
131
+ const timer2 = setTimeout(() => {
130
132
  cleanup();
131
133
  reject(new Error("Token refresh timeout"));
132
134
  }, REFRESH_TIMEOUT_MS);
133
135
  function onMessage(event) {
134
136
  if (event.origin !== expectedOrigin) return;
135
- if (!event.data || event.data.type !== AUTH_MESSAGE_TYPE) return;
137
+ if (!event.data) return;
138
+ if (event.data.type === AUTH_HANDOFF_READY_TYPE) {
139
+ if (handoffToken && iframe.contentWindow && event.source === iframe.contentWindow) {
140
+ iframe.contentWindow.postMessage({ type: AUTH_HANDOFF_TOKEN_TYPE, token: handoffToken }, expectedOrigin);
141
+ }
142
+ return;
143
+ }
144
+ if (event.data.type !== AUTH_MESSAGE_TYPE) return;
136
145
  cleanup();
137
146
  if (event.data.success) {
138
147
  resolve(event.data.data);
@@ -142,20 +151,20 @@ function openAuthIframe(url, expectedOrigin) {
142
151
  }
143
152
  function cleanup() {
144
153
  window.removeEventListener("message", onMessage);
145
- clearTimeout(timer);
154
+ clearTimeout(timer2);
146
155
  if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
147
156
  }
148
157
  window.addEventListener("message", onMessage);
149
158
  document.body.appendChild(iframe);
150
159
  });
151
160
  }
152
- async function refreshTokenSilently(authUrl, projectId) {
161
+ async function refreshTokenSilently(authUrl, projectId, currentToken) {
153
162
  if (_refreshPromise) return _refreshPromise;
154
163
  _refreshPromise = (async () => {
155
164
  try {
156
165
  const origin = getOriginFromUrl(authUrl);
157
- const url = buildAuthUrl(authUrl, "refresh", projectId);
158
- const response = await openAuthIframe(url, origin);
166
+ const url = buildAuthUrl(authUrl, "refresh", projectId) + (currentToken ? "&handoff=1" : "");
167
+ const response = await openAuthIframe(url, origin, currentToken);
159
168
  return response;
160
169
  } finally {
161
170
  _refreshPromise = null;
@@ -177,7 +186,7 @@ function openSilentAuthIframe(authUrl, projectId, credentials) {
177
186
  }
178
187
  const iframe = document.createElement("iframe");
179
188
  iframe.style.display = "none";
180
- const timer = setTimeout(() => {
189
+ const timer2 = setTimeout(() => {
181
190
  cleanup();
182
191
  reject(new Error("Timeout na autentica\xE7\xE3o silenciosa"));
183
192
  }, SILENT_TIMEOUT_MS);
@@ -201,7 +210,7 @@ function openSilentAuthIframe(authUrl, projectId, credentials) {
201
210
  }
202
211
  function cleanup() {
203
212
  window.removeEventListener("message", onMessage);
204
- clearTimeout(timer);
213
+ clearTimeout(timer2);
205
214
  if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
206
215
  }
207
216
  window.addEventListener("message", onMessage);
@@ -383,6 +392,105 @@ function withServerFunctionProof(headers, proof = readServerFunctionProof()) {
383
392
  return { ...headers, [SF_EXECUTION_TOKEN_HEADER]: proof };
384
393
  }
385
394
 
395
+ // src/session-watch.ts
396
+ var RENEW_BEFORE_MS = 6e4;
397
+ var RETRY_MS = 1e4;
398
+ var MAX_TIMEOUT_MS = 2147483647;
399
+ var refresher = null;
400
+ var timer = null;
401
+ var listening = false;
402
+ var expiryAtLastAttempt = null;
403
+ var notifiedFor = null;
404
+ function registerSessionRefresher(fn) {
405
+ refresher = fn;
406
+ }
407
+ function limitedSessionExpiry(token) {
408
+ if (!token) return null;
409
+ const payload = decodeJwtPayload(token);
410
+ if (!payload || typeof payload.authTime !== "number" || typeof payload.exp !== "number") return null;
411
+ return payload.exp * 1e3;
412
+ }
413
+ function currentExpiry() {
414
+ if (typeof window === "undefined" || !isConfigured()) return null;
415
+ const config = getConfig();
416
+ if (!config.token || !config.authUrl || config.projectId == null) return null;
417
+ return limitedSessionExpiry(config.token);
418
+ }
419
+ function clearTimer() {
420
+ if (timer) {
421
+ clearTimeout(timer);
422
+ timer = null;
423
+ }
424
+ }
425
+ function startSessionWatch() {
426
+ expiryAtLastAttempt = null;
427
+ notifiedFor = null;
428
+ watchSession();
429
+ }
430
+ function watchSession() {
431
+ clearTimer();
432
+ const expiry = currentExpiry();
433
+ if (expiry == null) return;
434
+ listenForReturn();
435
+ let delay = Math.max(expiry - Date.now() - RENEW_BEFORE_MS, 0);
436
+ if (expiryAtLastAttempt != null && expiry <= expiryAtLastAttempt + 1e3) {
437
+ delay = Math.max(expiry - Date.now() + 1e3, RETRY_MS);
438
+ }
439
+ if (delay > MAX_TIMEOUT_MS) return;
440
+ timer = setTimeout(() => {
441
+ void renewNow();
442
+ }, delay);
443
+ }
444
+ async function renewNow() {
445
+ clearTimer();
446
+ const before = currentExpiry();
447
+ if (before == null || !refresher) return;
448
+ expiryAtLastAttempt = before;
449
+ const renewed = await refresher().catch(() => false);
450
+ if (renewed) return;
451
+ const expiry = currentExpiry();
452
+ if (expiry != null && Date.now() < expiry) {
453
+ timer = setTimeout(() => {
454
+ void renewNow();
455
+ }, RETRY_MS);
456
+ }
457
+ }
458
+ function onReturn() {
459
+ const expiry = currentExpiry();
460
+ if (expiry == null) return;
461
+ const atSessionEnd = expiryAtLastAttempt != null && expiry <= expiryAtLastAttempt + 1e3;
462
+ if (expiry - Date.now() <= RENEW_BEFORE_MS && (!atSessionEnd || Date.now() >= expiry)) {
463
+ void renewNow();
464
+ } else {
465
+ watchSession();
466
+ }
467
+ }
468
+ function listenForReturn() {
469
+ if (listening || typeof window === "undefined") return;
470
+ listening = true;
471
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
472
+ document.addEventListener("visibilitychange", () => {
473
+ if (document.visibilityState === "visible") onReturn();
474
+ });
475
+ }
476
+ if (typeof window.addEventListener === "function") {
477
+ window.addEventListener("online", onReturn);
478
+ }
479
+ }
480
+ function reportRefreshFailure() {
481
+ var _a, _b;
482
+ const expiry = currentExpiry();
483
+ if (expiry == null || Date.now() < expiry) return;
484
+ const config = getConfig();
485
+ if (notifiedFor === config.token) return;
486
+ notifiedFor = (_a = config.token) != null ? _a : null;
487
+ clearTimer();
488
+ try {
489
+ (_b = config.onSessionExpired) == null ? void 0 : _b.call(config);
490
+ } catch (e) {
491
+ }
492
+ }
493
+
386
494
  // src/client.ts
387
495
  function getFetch2() {
388
496
  if (typeof globalThis.fetch === "function") {
@@ -417,14 +525,18 @@ function tryRefreshToken() {
417
525
  if (refreshInFlight === attempt) refreshInFlight = null;
418
526
  };
419
527
  attempt.then(clear, clear);
528
+ attempt.then((renewed) => {
529
+ if (!renewed) reportRefreshFailure();
530
+ }, () => void 0);
420
531
  return attempt;
421
532
  }
533
+ registerSessionRefresher(tryRefreshToken);
422
534
  async function doRefreshToken() {
423
535
  const config = getConfig();
424
536
  if (!config.token || config.projectId == null || !config.baseURL) return false;
425
537
  if (!config.authUrl || typeof window === "undefined") return false;
426
538
  try {
427
- const session = await refreshTokenSilently(config.authUrl, config.projectId);
539
+ const session = await refreshTokenSilently(config.authUrl, config.projectId, config.token);
428
540
  updateGlobalToken(session.token);
429
541
  if (config.onTokenRefresh) config.onTokenRefresh(session);
430
542
  return true;
@@ -711,6 +823,9 @@ async function callIntegrationMitra(options) {
711
823
  if (options.body !== void 0) {
712
824
  body.body = options.body;
713
825
  }
826
+ if (options.headers !== void 0) {
827
+ body.headers = options.headers;
828
+ }
714
829
  return http.post("/interactions/integrations/call", body);
715
830
  }
716
831
  async function stopServerFunctionExecutionMitra(options) {
@@ -1500,7 +1615,7 @@ function openSocket() {
1500
1615
  return new Promise((resolve, reject) => {
1501
1616
  const socket = new WebSocket(url);
1502
1617
  let opened = false;
1503
- const timer = setTimeout(() => {
1618
+ const timer2 = setTimeout(() => {
1504
1619
  if (!opened) {
1505
1620
  try {
1506
1621
  socket.close();
@@ -1511,13 +1626,13 @@ function openSocket() {
1511
1626
  }, 15e3);
1512
1627
  socket.onopen = () => {
1513
1628
  opened = true;
1514
- clearTimeout(timer);
1629
+ clearTimeout(timer2);
1515
1630
  ws = socket;
1516
1631
  resolve(socket);
1517
1632
  };
1518
1633
  socket.onerror = () => {
1519
1634
  if (!opened) {
1520
- clearTimeout(timer);
1635
+ clearTimeout(timer2);
1521
1636
  reject(new Error("Agent Chat: erro ao conectar no WebSocket"));
1522
1637
  }
1523
1638
  };
@@ -1525,7 +1640,7 @@ function openSocket() {
1525
1640
  if (ws === socket) ws = null;
1526
1641
  const detalhe = closeDetail(event);
1527
1642
  if (!opened) {
1528
- clearTimeout(timer);
1643
+ clearTimeout(timer2);
1529
1644
  reject(new Error(`Agent Chat: conex\xE3o fechada${detalhe}`));
1530
1645
  }
1531
1646
  for (const [id, pending] of pendingRequests) {
@@ -1900,7 +2015,7 @@ function createMitraInstance(initialConfig) {
1900
2015
  if (!_config.token || _config.projectId == null || !_config.baseURL) return false;
1901
2016
  if (!_config.authUrl || typeof window === "undefined") return false;
1902
2017
  try {
1903
- const session = await refreshTokenSilently(_config.authUrl, _config.projectId);
2018
+ const session = await refreshTokenSilently(_config.authUrl, _config.projectId, _config.token);
1904
2019
  _config = { ..._config, token: stripBearer(session.token) };
1905
2020
  updateGlobalToken(session.token);
1906
2021
  if (_config.onTokenRefresh) _config.onTokenRefresh(session);
@@ -2128,6 +2243,7 @@ function createMitraInstance(initialConfig) {
2128
2243
  if (options.endpoint !== void 0) body.endpoint = options.endpoint;
2129
2244
  if (options.params !== void 0) body.params = options.params;
2130
2245
  if (options.body !== void 0) body.body = options.body;
2246
+ if (options.headers !== void 0) body.headers = options.headers;
2131
2247
  return request("POST", "/interactions/integrations/call", { body });
2132
2248
  },
2133
2249
  // Dynamic Schema CRUD
@@ -2310,6 +2426,7 @@ function configureSdkMitra(config = {}) {
2310
2426
  const chat = window.__mitraChat;
2311
2427
  if (chat) chat.init(merged.token);
2312
2428
  }
2429
+ startSessionWatch();
2313
2430
  return createMitraInstance(globalConfig);
2314
2431
  }
2315
2432
  function getConfig() {
@@ -2324,6 +2441,7 @@ function isConfigured() {
2324
2441
  function updateGlobalToken(token) {
2325
2442
  if (globalConfig) {
2326
2443
  globalConfig = { ...globalConfig, token: stripBearer(token) };
2444
+ watchSession();
2327
2445
  }
2328
2446
  }
2329
2447
  function inferProjectId(projectId) {