@ciromaciel/auth-react 1.0.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/dist/index.js ADDED
@@ -0,0 +1,3594 @@
1
+ 'use strict';
2
+
3
+ var zustand = require('zustand');
4
+ var react = require('react');
5
+ var shallow = require('zustand/react/shallow');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+ var reactRouterDom = require('react-router-dom');
8
+ var core = require('@mantine/core');
9
+ var form = require('@mantine/form');
10
+ var iconsReact = require('@tabler/icons-react');
11
+
12
+ /**
13
+ * The identity switch — when the panel stops being one person and becomes
14
+ * another in the middle of a live session.
15
+ *
16
+ * It happens in both directions of an impersonation: entering (the response
17
+ * carries a `Set-Cookie` that changes who the server sees in that instant) and
18
+ * leaving (by the button, by the clock, or because another tab ended it). In
19
+ * every case the panel stays mounted for a short interval until the navigation
20
+ * — and during that interval the mounted screens refetch holding the old
21
+ * identity.
22
+ *
23
+ * The server answers those calls with 403 and 401, and it is right to: they are
24
+ * another person's resources, or a session that stopped being valid. What is
25
+ * wrong is SHOWING it — on screen it read as "Acesso negado: Sem permissão para
26
+ * esta organização" in the middle of a switch that was working.
27
+ *
28
+ * The flag lives on `window` because whoever sets it (the SDK) and whoever reads
29
+ * it (each panel's HTTP client) do not share a module. It is never turned off:
30
+ * the whole page is replaced right after, and `window` dies with it.
31
+ *
32
+ * Exported by the package so any panel can mark its own switches — the Auth
33
+ * panel marks the START of an impersonation, a moment only it knows about.
34
+ */
35
+
36
+ const FLAG = '__riligarSwitchingIdentity';
37
+
38
+ /**
39
+ * The event that announces: the person behind this session changed.
40
+ *
41
+ * The SDK knows WHEN the identity switches, but not what each panel keeps — and
42
+ * they all keep something account-shaped. The Auth panel persists the selected
43
+ * organization, and it was that persistence surviving the reload that made the
44
+ * panel ask for `/applications/<the operator's organization>` already as the
45
+ * target. The server refused with 403, correctly, and the screen showed "Acesso
46
+ * negado" AFTER the switch had finished — outside any transition window, which
47
+ * is why silencing the interceptor never fixed it.
48
+ *
49
+ * Each panel listens and clears what is its own. The SDK needs to know no key,
50
+ * and a new panel that persists something solves its own case without touching
51
+ * this file.
52
+ *
53
+ * Fired by `markIdentitySwitching`, which every identity switch already goes
54
+ * through — so a client of this package gets it by listening, with no call of
55
+ * its own to make and no knowledge of when a switch happens.
56
+ */
57
+ const IDENTITY_CHANGED_EVENT = 'riligar:identity-changed';
58
+
59
+ /**
60
+ * Announces the switch, giving listeners a chance to clear before the reload.
61
+ *
62
+ * Synchronous on purpose: `dispatchEvent` only returns once every listener has
63
+ * run, so the caller can reload on the next line knowing storage is already
64
+ * clean.
65
+ */
66
+ function announceIdentityChange(detail) {
67
+ try {
68
+ window.dispatchEvent(new CustomEvent(IDENTITY_CHANGED_EVENT, {
69
+ detail
70
+ }));
71
+ } catch {
72
+ // No `window` (SSR, tests): there is no panel to notify.
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Everything a panel remembers about an account, wiped on every switch.
78
+ *
79
+ * ALLOWLIST, NOT BLOCKLIST — and that is the whole point.
80
+ *
81
+ * The seven panels each persist something account-shaped, under names that
82
+ * follow no single pattern: `organizations` (the selected company),
83
+ * `hoster:pinned`, `rlg-payments-project-storage`,
84
+ * `riligar_storage_active_tenant`, per-product API keys. Listing them would be
85
+ * a blocklist, and a blocklist is wrong here in the way that hurts: a panel
86
+ * that persists something new is silently left out, and the next person to open
87
+ * that tab sees another account's data with nobody's name on it.
88
+ *
89
+ * So the rule is inverted. Everything goes, except the few keys that are about
90
+ * the BROWSER rather than about the person — the session token, which the SDK
91
+ * manages itself and rewrites on the next load, and per-device preferences that
92
+ * carry no account data.
93
+ *
94
+ * Dropping the rest costs a refetch. Keeping it costs showing one person's data
95
+ * under another person's name, which cannot be undone after it is on screen.
96
+ */
97
+ const KEEP = new Set([
98
+ // The SDK's own slot: cleared where it must be (ending an impersonation),
99
+ // and rewritten from the shared cookie on the next load. Wiping it here too
100
+ // would sign the operator out of a tab that only needed to forget the
101
+ // target's lists.
102
+ 'auth:token',
103
+ // Cross-tab logout beacon. It is a timestamp, not account data, and clearing
104
+ // it would break the very synchronisation this file exists to serve.
105
+ 'auth:logout',
106
+ // The switch beacon: a timestamp the sibling tabs listen for. Wiping it
107
+ // would make the NEXT write look unchanged to the browser in some cases,
108
+ // and the event that carries the switch would not fire.
109
+ 'auth:identity-switched']);
110
+ function dropStoredAccountState() {
111
+ try {
112
+ const doomed = [];
113
+ for (let i = 0; i < window.localStorage.length; i++) {
114
+ const k = window.localStorage.key(i);
115
+ if (k && !KEEP.has(k)) doomed.push(k);
116
+ }
117
+ for (const k of doomed) window.localStorage.removeItem(k);
118
+
119
+ // `sessionStorage` too: it survives a reload in the same tab, which is
120
+ // exactly the window this function exists to close.
121
+ window.sessionStorage?.clear();
122
+ } catch {
123
+ // No storage (SSR, private window, blocked cookies): there is nothing
124
+ // persisted to leak.
125
+ }
126
+ }
127
+
128
+ /**
129
+ * The beacon sibling tabs listen for.
130
+ *
131
+ * `storage` only fires in OTHER tabs of the same origin, and only when a value
132
+ * actually changes — so a timestamp is what turns "this tab switched identity"
133
+ * into an event the siblings receive. The SDK writes it, the SDK reads it
134
+ * (`AuthProvider`), and every panel that mounts the provider follows along
135
+ * without writing a line: that is what makes it work for our own seven panels
136
+ * and for anyone else's app on the same footing.
137
+ *
138
+ * It cannot reach ANOTHER origin — no browser mechanism can. Those tabs catch up
139
+ * through the poll, which asks the server on its own schedule.
140
+ */
141
+ const SWITCH_BEACON = 'auth:identity-switched';
142
+ function markIdentitySwitching() {
143
+ try {
144
+ window[FLAG] = true;
145
+ } catch {
146
+ // No `window` (SSR, tests): there is no panel to notify.
147
+ }
148
+ try {
149
+ // Written BEFORE the wipe below, because the wipe removes it again —
150
+ // and it is the write itself, not the value, that the siblings hear.
151
+ window.localStorage.setItem(SWITCH_BEACON, String(Date.now()));
152
+ } catch {
153
+ // No storage: the sibling tabs fall back to the poll.
154
+ }
155
+ // Applies to EVERY identity switch — entering and leaving an impersonation —
156
+ // because in both directions everything remembered starts belonging to
157
+ // another account.
158
+ dropStoredAccountState();
159
+
160
+ /*
161
+ * Wiping storage is not enough on its own: a store that keeps its state in
162
+ * MEMORY and only mirrors it to storage — every `zustand/persist` store is
163
+ * one — survives the wipe untouched, and rewrites the old value on its next
164
+ * `set()`. That is the whole bug this event exists for: the Auth panel's
165
+ * selected organization stayed in memory across the switch, and the screen
166
+ * asked for `/applications/<the target's organization>` as the operator,
167
+ * which the server refused with 403 — correctly.
168
+ *
169
+ * Announced LAST, so a listener that reads storage sees it already clean.
170
+ */
171
+ announceIdentityChange({
172
+ reason: 'switch'
173
+ });
174
+ }
175
+ function clearIdentitySwitching() {
176
+ try {
177
+ window[FLAG] = false;
178
+ } catch {
179
+ // Same reasoning as above.
180
+ }
181
+ }
182
+ function isIdentitySwitching() {
183
+ try {
184
+ return Boolean(window[FLAG]);
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Should a 401 sign the person out — or is it the expected 401 of a switch?
192
+ *
193
+ * Every panel's HTTP client reacts to a 401 by signing out. On the server that
194
+ * `signOut` deletes EVERY session of the person's e-mail across all products
195
+ * (one account, one e-mail, many application rows) — the right thing for a real
196
+ * "sign out everywhere", and a catastrophe for a transient 401.
197
+ *
198
+ * Leaving an impersonation keeps the panel mounted for a moment while it
199
+ * reloads, and calls already in flight carry the dying impersonation token. The
200
+ * server answers 401, correctly. Acting on THAT 401 signed the operator out of
201
+ * everything — the account came back for a second and was then wiped by its own
202
+ * panel. Exiting from the Auth tab happened to dodge it; exiting from Functions,
203
+ * Monitors or Hoster did not, which is why it looked panel-specific.
204
+ *
205
+ * Pure and exported so each panel shares one decision instead of three copies of
206
+ * an inline `&& !isIdentitySwitching()`, and so a single test covers all of
207
+ * them. Pass the flag in (do not read it here) to keep it testable without a
208
+ * `window`.
209
+ */
210
+ function shouldSignOutOn401(switching) {
211
+ return !switching;
212
+ }
213
+
214
+ // The default this package resolves to when an integration does not pass a URL.
215
+ // It ships inside the published bundle, so a stale value here fails on the
216
+ // customer's machine, not on ours.
217
+ let API_BASE = 'https://auth.worker.myinfrastructure.click';
218
+ let API_KEY = null;
219
+ let INTERNAL_MODE = false; // Internal mode: no API key required (same-domain apps)
220
+
221
+ // Permite configurar API key e modo interno externamente (chamado pelo AuthProvider)
222
+ function configure({
223
+ apiKey,
224
+ apiUrl,
225
+ internal = false
226
+ }) {
227
+ if (apiKey) API_KEY = apiKey;
228
+ if (apiUrl) API_BASE = apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl;
229
+ INTERNAL_MODE = internal;
230
+ }
231
+ const isInternal = () => INTERNAL_MODE;
232
+
233
+ /** A URL da API de autenticação em uso. É a origem confiável por definição. */
234
+ const getApiUrl = () => API_BASE;
235
+
236
+ /** A chave onde o token de sessão é guardado. Exposta para quem precisa lê-lo. */
237
+ const TOKEN_STORAGE_KEY = 'auth:token';
238
+
239
+ // helper fetch pré-configurado
240
+ async function api(route, opts = {}) {
241
+ // Garante que a rota comece com /
242
+ const cleanRoute = route.startsWith('/') ? route : `/${route}`;
243
+
244
+ // Constrói URL completa (API_BASE já teve trailing slash removido no configure)
245
+ const url = `${API_BASE}${cleanRoute}`;
246
+ const token = getStoredToken();
247
+ const headers = {
248
+ 'Content-Type': 'application/json',
249
+ Accept: 'application/json',
250
+ ...opts.headers
251
+ };
252
+
253
+ // Adiciona Authorization header se tiver token
254
+ if (token) {
255
+ headers.Authorization = `Bearer ${token}`;
256
+ }
257
+
258
+ // Adiciona API Key se configurada e não estiver em modo interno
259
+ if (API_KEY && !INTERNAL_MODE) {
260
+ headers['X-API-Key'] = API_KEY;
261
+ }
262
+ const res = await fetch(url, {
263
+ headers,
264
+ credentials: 'include',
265
+ // Required for sending session cookies
266
+ ...opts
267
+ });
268
+
269
+ // Converte JSON automaticamente e lança erro legível
270
+ const data = res.status !== 204 ? await res.json().catch(() => ({})) : null;
271
+ if (!res.ok) {
272
+ /*
273
+ * O contrato: `{error: {code, message, details?}}`.
274
+ *
275
+ * `code` é o identificador estável — programe contra ele. `message` é
276
+ * a frase para o humano e pode mudar de redação a qualquer momento.
277
+ * `details` traz o que dá para agir (o limite estourado, o campo que
278
+ * faltou), e vai no Error para quem trata sem precisar do corpo.
279
+ */
280
+ const payloadError = data?.error;
281
+ const failure = new Error(payloadError?.message || res.statusText);
282
+ failure.res = res;
283
+ failure.data = data;
284
+ failure.status = res.status;
285
+ failure.code = payloadError?.code ?? null;
286
+ failure.details = payloadError?.details ?? null;
287
+ failure.retriable = Boolean(payloadError?.retriable);
288
+ throw failure;
289
+ }
290
+ return data;
291
+ }
292
+
293
+ // Gerenciamento de token no localStorage
294
+ function getStoredToken() {
295
+ if (typeof window === 'undefined') return null;
296
+ return window.localStorage.getItem(TOKEN_STORAGE_KEY);
297
+ }
298
+
299
+ // Exportada: o store precisa DESCARTAR o token quando o servidor não
300
+ // reconhece a sessão. Sem isso, um valor obsoleto sobrevive no localStorage e
301
+ // o handoff do OAuth o envia no `#token=`, derrubando a autorização.
302
+ function setStoredToken(token) {
303
+ if (typeof window === 'undefined') return;
304
+ if (token) {
305
+ window.localStorage.setItem(TOKEN_STORAGE_KEY, token);
306
+ } else {
307
+ window.localStorage.removeItem(TOKEN_STORAGE_KEY);
308
+ }
309
+ }
310
+ // Helper para processar resposta de autenticação e salvar token
311
+ function handleAuthResponse(result) {
312
+ // Tenta encontrar o token em vários lugares possíveis
313
+ const token = result.token || result.session?.token || result.session?.sessionToken;
314
+ if (token) {
315
+ setStoredToken(token);
316
+ }
317
+ return result;
318
+ }
319
+
320
+ // Decodifica JWT (apenas payload, sem verificação de assinatura)
321
+ function decodeJWT(token) {
322
+ try {
323
+ const parts = token.split('.');
324
+ if (parts.length !== 3) return null;
325
+
326
+ // Safe base64 decode
327
+ const base64Url = parts[1];
328
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
329
+
330
+ // Verifica ambiente para decodificar
331
+ const jsonPayload = typeof window !== 'undefined' ? window.atob(base64) : Buffer.from(base64, 'base64').toString();
332
+ return JSON.parse(jsonPayload);
333
+ } catch {
334
+ return null;
335
+ }
336
+ }
337
+
338
+ // Verifica se o token está expirado
339
+ function isTokenExpired(token) {
340
+ const payload = decodeJWT(token);
341
+
342
+ // Se não for um JWT válido (payload null), assumimos que é um token opaco (session token)
343
+ // Nesse caso, deixamos o servidor validar via 401
344
+ if (!payload) return false;
345
+ if (!payload.exp) return false;
346
+ const now = Date.now();
347
+ const exp = payload.exp * 1000;
348
+ const isExpired = now >= exp;
349
+ if (isExpired) {
350
+ console.log('[AuthSDK] Token expired:', {
351
+ now,
352
+ exp,
353
+ diff: exp - now
354
+ });
355
+ }
356
+ return isExpired;
357
+ }
358
+
359
+ // Verifica se o usuário está autenticado
360
+ function isAuthenticated() {
361
+ const token = getStoredToken();
362
+ const valid = token && !isTokenExpired(token);
363
+ return valid;
364
+ }
365
+
366
+ // Obtém dados do usuário do token
367
+ function getCurrentUser() {
368
+ const token = getStoredToken();
369
+ if (!token || isTokenExpired(token)) return null;
370
+ const payload = decodeJWT(token);
371
+ return payload ? {
372
+ id: payload.sub,
373
+ email: payload.email,
374
+ name: payload.name,
375
+ ...payload
376
+ } : null;
377
+ }
378
+
379
+ /*--- sign in: ask for the code, trade the code ----*/
380
+ //
381
+ // These are the only two calls that authenticate. There is no separate sign-up:
382
+ // someone who never signed in and someone who already did walk exactly the same
383
+ // path, and `requestCode`'s answer is the same in both cases — it does not tell
384
+ // whether the email already had an account.
385
+
386
+ /**
387
+ * Asks for the sign-in code. The email goes out right away; the response does
388
+ * not wait for it.
389
+ *
390
+ * @param {string} email
391
+ * @param {{ name?: string }} [options] Name used only if the account is created now.
392
+ * @returns {Promise<{ sent: boolean, expiresIn: number, interval: number, deviceCode: string }>}
393
+ */
394
+ const requestCode = async (email, {
395
+ name
396
+ } = {}) => {
397
+ return await api('/auth/code/start', {
398
+ method: 'POST',
399
+ body: JSON.stringify({
400
+ email,
401
+ ...(name ? {
402
+ name
403
+ } : {})
404
+ })
405
+ });
406
+ };
407
+
408
+ /**
409
+ * Trades the code for the session. This is the whole sign-in.
410
+ *
411
+ * @param {string} email The same email that asked for the code.
412
+ * @param {string} code As the person typed it — case, spaces and the hyphen are optional.
413
+ * @returns {Promise<{ user: object, token: string, session: object }>}
414
+ */
415
+ const verifyCode = async (email, code) => {
416
+ const result = await api('/auth/code/verify', {
417
+ method: 'POST',
418
+ body: JSON.stringify({
419
+ email,
420
+ code
421
+ })
422
+ });
423
+ return handleAuthResponse(result);
424
+ };
425
+
426
+ /**
427
+ * Variant for whoever CANNOT read the email — an agent waiting for someone else
428
+ * to present the code. Returns `{ pending: true, interval }` while nobody has
429
+ * approved; in the browser, use `verifyCode`.
430
+ *
431
+ * @param {string} deviceCode The value returned by `requestCode`.
432
+ */
433
+ const pollCode = async deviceCode => {
434
+ try {
435
+ const result = await api('/auth/code/poll', {
436
+ method: 'POST',
437
+ body: JSON.stringify({
438
+ deviceCode
439
+ })
440
+ });
441
+ return handleAuthResponse(result);
442
+ } catch (error) {
443
+ // 428/429 are not failures: they mean "not yet" and "slow down".
444
+ // Whoever polls needs to tell those apart from a dead code, which is a
445
+ // real error.
446
+ if (error?.status === 428 || error?.status === 429) {
447
+ return {
448
+ pending: true,
449
+ interval: error?.details?.interval ?? 5
450
+ };
451
+ }
452
+ throw error;
453
+ }
454
+ };
455
+ const signOut = async () => {
456
+ try {
457
+ await api('/auth/sign-out', {
458
+ method: 'POST'
459
+ });
460
+ } catch {
461
+ // Ignores sign-out errors on the server
462
+ } finally {
463
+ setStoredToken(null);
464
+ }
465
+ };
466
+ const refreshToken = async () => {
467
+ try {
468
+ const result = await api('/auth/refresh', {
469
+ method: 'POST'
470
+ });
471
+ return handleAuthResponse(result);
472
+ } catch (error) {
473
+ setStoredToken(null);
474
+ throw error;
475
+ }
476
+ };
477
+
478
+ /*--- Impersonation ------------------------------*/
479
+ /*
480
+ * Encerra a impersonação em curso.
481
+ *
482
+ * Não manda token nem id de sessão: o cookie de impersonação viaja sozinho
483
+ * (`credentials: 'include'`) e é ele que autoriza. Quem está dentro da
484
+ * impersonação é, por definição, quem pode fechá-la.
485
+ *
486
+ * O servidor responde limpando o cookie; depois disso o painel volta a ser o
487
+ * operador sozinho, e um reload basta para a tela acompanhar.
488
+ */
489
+ const endImpersonation = async impersonationId => api(`/impersonation/${impersonationId}/end`, {
490
+ method: 'POST'
491
+ });
492
+
493
+ /*--- Session ------------------------------------*/
494
+ // O cookie `riligar.session_token` é compartilhado por `.myinfrastructure.click`, então
495
+ // um painel recém-aberto autentica aqui SEM ter nada no localStorage (que é
496
+ // isolado por origem). Nesse caso semeamos o token local a partir da resposta:
497
+ // `isAuthenticated()` e o refresh em background leem do localStorage, e sem
498
+ // isto o painel ficaria autenticado no servidor mas "deslogado" no cliente —
499
+ // e o token venceria sem nunca ser renovado naquela origem.
500
+ // A gravação é INCONDICIONAL, e não só quando o slot está vazio. A chave é
501
+ // fixa por origem, então um token de sessão já encerrada sobrevive a logout,
502
+ // troca de conta e expiração — e `if (!getStoredToken())` nunca o substituía.
503
+ // O servidor acabou de dizer qual é a sessão desta requisição; ele é a
504
+ // autoridade, e o valor local que discorda dele é resíduo.
505
+ //
506
+ // Importa além do painel: o handoff do OAuth lê ESTE valor para montar o
507
+ // `#token=`, então um resíduo aqui derruba a autorização inteira com
508
+ // "Sessão inválida ou expirada", num ponto que não aponta para a causa.
509
+ const getSession = async () => {
510
+ const result = await api('/auth/session');
511
+ const token = result?.token || result?.session?.token;
512
+ if (token && token !== getStoredToken()) setStoredToken(token);
513
+ return result;
514
+ };
515
+ const listSessions = async () => {
516
+ // As SESSÕES, não o envelope: a assinatura sempre devolveu a lista, e é a
517
+ // ergonomia certa para quem só quer renderizá-las.
518
+ const body = await api('/auth/list-sessions');
519
+ return Array.isArray(body?.items) ? body.items : [];
520
+ };
521
+ const revokeSession = async id => {
522
+ return await api('/auth/revoke-session-by-id', {
523
+ method: 'POST',
524
+ body: JSON.stringify({
525
+ id
526
+ })
527
+ });
528
+ };
529
+ const revokeOtherSessions = async () => {
530
+ return await api('/auth/revoke-other-sessions', {
531
+ method: 'POST'
532
+ });
533
+ };
534
+
535
+ /*--- Application Info ----------------------------*/
536
+ const getApplicationInfo = async () => {
537
+ try {
538
+ // O recurso vem na RAIZ.
539
+ return (await api('/application/by-api-key')) || null;
540
+ } catch (error) {
541
+ console.warn('[AuthSDK] Failed to fetch application info:', error.message);
542
+ return null;
543
+ }
544
+ };
545
+
546
+ /*--- Profile Management ---------------------------*/
547
+ const updateProfile = async data => {
548
+ return await api('/auth/update-user', {
549
+ method: 'POST',
550
+ body: JSON.stringify(data)
551
+ });
552
+ };
553
+
554
+ /*--- Social sign-in -------------------------------------------------------*/
555
+
556
+ /**
557
+ * The providers this application has enabled.
558
+ *
559
+ * Public: it answers a screen where nobody is signed in yet. It carries only
560
+ * `{ provider, name }` — never the client id, which identifies the owner's
561
+ * project at Google.
562
+ *
563
+ * Returns `[]` on failure rather than throwing: a sign-in screen that cannot
564
+ * reach this endpoint must still render the email field, which is the path that
565
+ * always works.
566
+ */
567
+ const getSocialProviders = async () => {
568
+ try {
569
+ const response = await api('/auth/providers');
570
+ return response?.items || [];
571
+ } catch (error) {
572
+ console.warn('[AuthSDK] Failed to fetch social providers:', error.message);
573
+ return [];
574
+ }
575
+ };
576
+
577
+ /**
578
+ * Leaves for the provider.
579
+ *
580
+ * A full-page navigation, not `fetch`: the person has to SEE Google's consent
581
+ * screen, and an XHR would be blocked by CORS anyway. The API key travels in the
582
+ * query string because a redirect carries no headers — it is the public `pu_`,
583
+ * which is already in the bundle.
584
+ *
585
+ * `redirect` is where to come back to; the worker validates it against the
586
+ * application's allowlist BEFORE leaving, and stores the validated value. It
587
+ * defaults to the current page.
588
+ */
589
+ const startSocialSignIn = (provider, {
590
+ redirect
591
+ } = {}) => {
592
+ const destination = redirect || window.location.href.split('#')[0];
593
+ const url = new URL(`${API_BASE}/auth/sign-in/${provider}`);
594
+ url.searchParams.set('redirect', destination);
595
+ if (API_KEY && !INTERNAL_MODE) url.searchParams.set('api_key', API_KEY);
596
+ window.location.assign(url.toString());
597
+ };
598
+
599
+ /**
600
+ * Reads the token the callback left in the fragment, and cleans the URL.
601
+ *
602
+ * The token comes back after `#` precisely so it never reaches a server, a log
603
+ * or a `Referer`. Once read it is removed from the address bar with
604
+ * `replaceState`, so a copied URL does not carry a live session.
605
+ *
606
+ * Returns the token when there was one, `null` otherwise.
607
+ */
608
+ const consumeSocialToken = () => {
609
+ if (typeof window === 'undefined' || !window.location.hash) return null;
610
+ const params = new URLSearchParams(window.location.hash.slice(1));
611
+ const token = params.get('token');
612
+ if (!token) return null;
613
+
614
+ /*
615
+ * A social sign-in can land on a tab that already belongs to SOMEBODY ELSE.
616
+ *
617
+ * Coming back from Google replaces the session, and the person behind it may
618
+ * not be the one who left: signing in with a Google account whose email has
619
+ * no user here creates a NEW user, with an organization of its own.
620
+ *
621
+ * Swapping the token alone is not enough. Every panel keeps account-shaped
622
+ * state — the Auth panel persists the selected organization — and a
623
+ * `zustand/persist` store holds it in MEMORY, where clearing storage does
624
+ * not reach. The stale organization then rides the NEW token into the next
625
+ * request, and the server answers 403 "Sem permissão para esta organização"
626
+ * — correctly, about a switch that actually worked.
627
+ *
628
+ * That is the exact failure `markIdentitySwitching` exists for, and which
629
+ * impersonation already routes through. Social sign-in is one more identity
630
+ * switch and has to go through it too.
631
+ *
632
+ * Compared by `sub`, not by token string: a refresh rotates the token for
633
+ * the SAME person, and treating that as a switch would wipe their panel
634
+ * state for nothing.
635
+ */
636
+ const previous = getStoredToken();
637
+ if (previous && previous !== token) {
638
+ const before = decodeJWT(previous)?.sub;
639
+ const after = decodeJWT(token)?.sub;
640
+ // Only when we can read both and they disagree. An unreadable token is
641
+ // not evidence of a switch, and guessing here would clear state on a
642
+ // malformed value.
643
+ if (before && after && before !== after) markIdentitySwitching();
644
+ }
645
+ setStoredToken(token);
646
+ params.delete('token');
647
+ const rest = params.toString();
648
+ window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}${rest ? `#${rest}` : ''}`);
649
+ return token;
650
+ };
651
+
652
+ /**
653
+ * The failure the callback reported, if any. Also clears it from the URL.
654
+ *
655
+ * The worker sends a short opaque code — never the provider's own error text,
656
+ * which echoes back parts of our request. Phrasing lives here, next to the other
657
+ * user-facing strings.
658
+ */
659
+ const consumeSocialError = () => {
660
+ if (typeof window === 'undefined') return null;
661
+ const params = new URLSearchParams(window.location.search);
662
+ const reason = params.get('social_error');
663
+ if (!reason) return null;
664
+ params.delete('social_error');
665
+ const rest = params.toString();
666
+ window.history.replaceState(null, '', `${window.location.pathname}${rest ? `?${rest}` : ''}${window.location.hash}`);
667
+ return reason;
668
+ };
669
+
670
+ /** Starts linking a provider to the account of the CURRENT session. */
671
+ const startSocialLink = async (provider, {
672
+ redirect
673
+ } = {}) => {
674
+ const destination = redirect || window.location.href.split('#')[0];
675
+ const response = await api(`/auth/link/${provider}`, {
676
+ method: 'POST',
677
+ body: JSON.stringify({
678
+ redirect: destination
679
+ })
680
+ });
681
+ if (response?.authorizeUrl) window.location.assign(response.authorizeUrl);
682
+ return response;
683
+ };
684
+ const unlinkSocialProvider = async provider => {
685
+ return await api(`/auth/unlink/${provider}`, {
686
+ method: 'POST'
687
+ });
688
+ };
689
+
690
+ /** The providers linked to the current session's user. */
691
+ const getLinkedProviders = async () => {
692
+ const response = await api('/auth/linked-providers');
693
+ return response?.items || [];
694
+ };
695
+
696
+ // ============================================================================
697
+ // REDIRECT PÓS-LOGIN
698
+ // ============================================================================
699
+ //
700
+ // Quem inicia um fluxo OAuth (o servidor MCP, por exemplo) manda o usuário ao
701
+ // painel com `?redirect=` apontando de volta. Sem tratar esse parâmetro, o
702
+ // login termina na home do painel e a autorização fica órfã.
703
+ //
704
+ // A lógica vive aqui, e não em cada aplicação, porque ela tem duas armadilhas
705
+ // que não se quer reimplementar três vezes:
706
+ //
707
+ // 1. `redirect` sem allowlist transforma a tela de login em open redirect —
708
+ // um link `?redirect=https://phishing.example` levaria o usuário para lá
709
+ // logo após ele digitar a senha, num domínio que ele confia.
710
+ //
711
+ // 2. O token vive em `localStorage`, que é isolado POR ORIGEM. O worker do
712
+ // Auth (auth.worker.*) não consegue lê-lo do painel (produto.dashboard.*),
713
+ // então ele precisa ser entregue — e o fragmento (`#token=`) é o único
714
+ // canal que não chega ao servidor nem entra em log de acesso.
715
+ // ============================================================================
716
+
717
+
718
+ /**
719
+ * Origens para as quais é seguro redirecionar após o login.
720
+ *
721
+ * A origem da própria API entra sempre: é para lá que o fluxo OAuth volta, e
722
+ * o SDK já a conhece por `configure({ apiUrl })`. A origem atual também, por
723
+ * ser a mesma página. `extraOrigins` cobre o resto (um site institucional que
724
+ * inicie o login, por exemplo).
725
+ */
726
+ function allowedOrigins(extraOrigins = []) {
727
+ const list = [];
728
+ try {
729
+ list.push(new URL(getApiUrl()).origin);
730
+ } catch {}
731
+ if (typeof window !== 'undefined') list.push(window.location.origin);
732
+ for (const raw of extraOrigins) {
733
+ try {
734
+ list.push(new URL(raw).origin);
735
+ } catch {}
736
+ }
737
+ return list;
738
+ }
739
+ const isLocalhost = hostname => hostname === 'localhost' || hostname === '127.0.0.1';
740
+
741
+ /**
742
+ * Valida o `redirect` recebido e devolve o destino, ou `null`.
743
+ *
744
+ * Caminho interno (`/algo`) passa sempre — não sai do domínio. URL absoluta só
745
+ * passa se a origem constar na allowlist e for HTTPS: aceitar `http://` num
746
+ * host permitido exporia o token em trânsito.
747
+ */
748
+ function resolveRedirect(raw, extraOrigins = []) {
749
+ if (!raw || typeof raw !== 'string') return null;
750
+
751
+ // `//` seria protocol-relative, que sai do domínio apesar de parecer path.
752
+ if (raw.startsWith('/') && !raw.startsWith('//')) return raw;
753
+ let url;
754
+ try {
755
+ url = new URL(raw);
756
+ } catch {
757
+ return null;
758
+ }
759
+
760
+ // Localhost passa em desenvolvimento: o destino é a máquina do próprio
761
+ // usuário, não um servidor de terceiro.
762
+ if (isLocalhost(url.hostname)) return raw;
763
+ if (url.protocol !== 'https:') return null;
764
+ return allowedOrigins(extraOrigins).includes(url.origin) ? raw : null;
765
+ }
766
+
767
+ /**
768
+ * Executa o redirect pós-login.
769
+ *
770
+ * Devolve `true` quando assumiu a navegação — o chamador então não deve
771
+ * navegar por conta própria. `false` significa "não havia redirect válido,
772
+ * siga o seu fluxo normal".
773
+ *
774
+ * @param {string|null} target destino já validado por `resolveRedirect`
775
+ * @param {(path: string) => void} navigate roteador da aplicação, para paths internos
776
+ * @param {boolean} withToken anexar o token no fragmento (necessário quando o
777
+ * destino é outra origem que precisa da sessão, como o handoff do OAuth)
778
+ */
779
+ function applyRedirect(target, navigate, {
780
+ withToken = true
781
+ } = {}) {
782
+ if (!target) return false;
783
+
784
+ // Caminho interno: o roteador resolve, sem recarregar a página.
785
+ if (target.startsWith('/')) {
786
+ navigate?.(target, {
787
+ replace: true
788
+ });
789
+ return true;
790
+ }
791
+ if (typeof window === 'undefined') return false;
792
+ let finalUrl = target;
793
+ if (withToken) {
794
+ const token = window.localStorage.getItem(TOKEN_STORAGE_KEY);
795
+ // O fragmento nunca é enviado ao servidor: não aparece em log de
796
+ // acesso nem no Referer. É o canal certo para um token.
797
+ if (token) finalUrl = `${target}#token=${encodeURIComponent(token)}`;
798
+ }
799
+ window.location.replace(finalUrl);
800
+ return true;
801
+ }
802
+
803
+ /**
804
+ * Lê o `redirect` da URL atual, valida e devolve o destino (ou `null`).
805
+ *
806
+ * Atalho para o caso comum: o componente não precisa mexer em `URLSearchParams`
807
+ * nem lembrar o nome do parâmetro.
808
+ */
809
+ function getRedirectFromLocation(extraOrigins = [], paramName = 'redirect') {
810
+ if (typeof window === 'undefined') return null;
811
+ const raw = new URLSearchParams(window.location.search).get(paramName);
812
+ return resolveRedirect(raw, extraOrigins);
813
+ }
814
+
815
+ // Shortest gap between two focus-driven session reads.
816
+ const REVALIDATE_THROTTLE_MS = 5000;
817
+
818
+ // Estado: { user, loading, error }
819
+ const useAuthStore = zustand.create((set, get) => ({
820
+ user: null,
821
+ loading: true,
822
+ error: null,
823
+ // Timestamp of the last focus revalidation, for throttling.
824
+ lastRevalidatedAt: 0,
825
+ // Session management
826
+ sessions: [],
827
+ currentSession: null,
828
+ // Loading states granulares
829
+ loadingStates: {
830
+ requestCode: false,
831
+ verifyCode: false,
832
+ signOut: false,
833
+ updateProfile: false,
834
+ listSessions: false,
835
+ revokeSession: null // null or sessionId being revoked
836
+ },
837
+ // Application info (logo, nome, etc)
838
+ applicationInfo: null,
839
+ /*
840
+ * Preenchido quando ESTA sessão é emprestada — `/auth/session` devolve
841
+ * `{ actor, expiresAt }`. É o que a barra lê para avisar, em qualquer painel,
842
+ * que quem está logado não é quem está olhando.
843
+ */
844
+ impersonation: null,
845
+ // Helper para atualizar loading states
846
+ setLoading: (key, value) => set(state => ({
847
+ loadingStates: {
848
+ ...state.loadingStates,
849
+ [key]: value
850
+ }
851
+ })),
852
+ // Buscar informações da aplicação
853
+ fetchApplicationInfo: async () => {
854
+ try {
855
+ const appInfo = await getApplicationInfo();
856
+ set({
857
+ applicationInfo: appInfo
858
+ });
859
+ } catch (error) {
860
+ console.warn('[AuthStore] Failed to fetch application info:', error);
861
+ set({
862
+ applicationInfo: null
863
+ });
864
+ }
865
+ },
866
+ /*
867
+ * Reads the session from the server and mirrors it into the store.
868
+ *
869
+ * Shared by `init` (on mount) and `revalidate` (on focus). `trusted` says
870
+ * whether a failure is allowed to sign the user out: on mount there is
871
+ * nothing to lose, but on a revalidation the user is already signed in and
872
+ * a flaky network must not drop them.
873
+ */
874
+ syncSession: async ({
875
+ trusted
876
+ }) => {
877
+ try {
878
+ // Busca a sessão (que agora também traz applicationInfo)
879
+ const sessionData = await getSession();
880
+
881
+ // Se veio aplicação no redirecionamento/sessão, salva no store
882
+ if (sessionData?.application) {
883
+ set({
884
+ applicationInfo: sessionData.application
885
+ });
886
+ }
887
+
888
+ // `?? null` e não `|| undefined`: a ausência do campo é a resposta
889
+ // normal (ninguém impersonando) e tem que LIMPAR o estado anterior,
890
+ // senão a barra sobrevive ao encerramento.
891
+ set({
892
+ impersonation: sessionData?.impersonation ?? null
893
+ });
894
+ const user = sessionData?.user ?? null;
895
+ if (sessionData?.session) {
896
+ set({
897
+ currentSession: sessionData.session
898
+ });
899
+ }
900
+
901
+ // Se não encontrou sessão via cookies, verifica localStorage token (JWT)
902
+ //
903
+ // `isAuthenticated()` só confere o `exp` do JWT — não fala com o
904
+ // servidor. Uma sessão revogada tem `exp` no futuro e passaria por
905
+ // aqui: o painel se mostraria logado com um token que o servidor já
906
+ // não reconhece. Como o handoff do OAuth envia ESTE token no
907
+ // `#token=`, o resíduo derrubava a autorização com "Sessão inválida
908
+ // ou expirada" — longe da causa.
909
+ //
910
+ // O servidor acabou de responder sem sessão; ele é a autoridade.
911
+ if (!user) {
912
+ /*
913
+ * "No user" WITH a stored token is not a logout — it is a token
914
+ * that stopped being valid. The right answer is to ask again,
915
+ * without it.
916
+ *
917
+ * This is the path of whoever ended the impersonation in ANOTHER
918
+ * tab. `/auth/session` with a dead token answers 200 and WITHOUT
919
+ * a user (not 401: a session that does not exist is the normal
920
+ * "nobody signed in" path). This branch then concluded "logged
921
+ * out" and stopped there — while the operator's session was
922
+ * alive in the cookie, shared across the zone and never touched.
923
+ *
924
+ * The retry goes without `Authorization`, the cookie speaks, and
925
+ * the operator becomes themselves again. One attempt only: if
926
+ * this one also comes back without a user, there is no session
927
+ * behind it and logging out is correct.
928
+ */
929
+ if (isAuthenticated()) {
930
+ setStoredToken(null);
931
+ const semToken = await getSession().catch(() => null);
932
+ if (semToken?.user) {
933
+ const anterior = get().user;
934
+ const trocou = anterior && anterior.id !== semToken.user.id;
935
+ set({
936
+ user: semToken.user,
937
+ currentSession: semToken.session ?? null,
938
+ impersonation: semToken.impersonation ?? null,
939
+ loading: false
940
+ });
941
+ if (trocou && typeof window !== 'undefined') {
942
+ markIdentitySwitching();
943
+ window.location.reload();
944
+ }
945
+ return;
946
+ }
947
+ }
948
+ set({
949
+ user: null,
950
+ currentSession: null,
951
+ impersonation: null,
952
+ loading: false
953
+ });
954
+ return;
955
+ }
956
+
957
+ /*
958
+ * THE IDENTITY CHANGED UNDER THE SCREEN: drop everything and reload.
959
+ *
960
+ * This is the guard that makes the server the single truth. The
961
+ * seven panels hold data in memory and in their own stores —
962
+ * projects, lists, counters — and none of them knows when the person
963
+ * behind the session stopped being the same one.
964
+ *
965
+ * It happens for real when an impersonation is ended in ANOTHER tab:
966
+ * `localStorage` is per origin, so Hoster never learns that Functions
967
+ * ended it. On the next focus it revalidates, the server answers with
968
+ * the operator — and the screen was left with one person's footer and
969
+ * another's list, two people's data at once.
970
+ *
971
+ * Swapping `user` in the store would not be enough: whoever already
972
+ * read the list does not read it again. Reloading is the only way to
973
+ * guarantee that NOTHING of the previous identity survives — and it
974
+ * is cheap, because it only happens on the transition.
975
+ *
976
+ * `previous` comes from the state, not from a module variable: under
977
+ * SSR and in tests the module is shared, and the comparison has to be
978
+ * per store.
979
+ */
980
+ const previous = get().user;
981
+ const identityChanged = previous && previous.id !== user.id;
982
+ if (identityChanged && typeof window !== 'undefined') {
983
+ /*
984
+ * Mark the switch BEFORE reloading.
985
+ *
986
+ * The reload is not instantaneous: in-flight calls finish, and
987
+ * mounted screens still fire their own. All of them carry the old
988
+ * identity and get 403 — which surfaced as "Acesso negado: Sem
989
+ * permissão para esta organização" in the middle of a switch that
990
+ * was working.
991
+ *
992
+ * This guard reloaded without marking, and was the last place the
993
+ * notification escaped through.
994
+ */
995
+ markIdentitySwitching();
996
+ set({
997
+ user,
998
+ loading: false
999
+ });
1000
+ window.location.reload();
1001
+ return;
1002
+ }
1003
+ set({
1004
+ user,
1005
+ loading: false
1006
+ });
1007
+ } catch (error) {
1008
+ // Só descartamos o token quando o SERVIDOR o recusou (401). Uma
1009
+ // falha de rede não diz nada sobre a validade da sessão, e limpar
1010
+ // aqui deslogaria quem só perdeu conexão por um instante.
1011
+ const rejected = error?.res?.status === 401;
1012
+
1013
+ /*
1014
+ * Token refused: drop it AND ASK AGAIN, without it.
1015
+ *
1016
+ * Clearing alone was not enough, and that is what made the operator
1017
+ * lose their account when ending an impersonation in ANOTHER tab.
1018
+ * `localStorage` is per origin: ending in Functions does not remove
1019
+ * the target's token stored in Hoster and Auth. Coming back to that
1020
+ * tab, it revalidated with a dead token, took a 401, cleared it — and
1021
+ * stopped there, concluding "logged out".
1022
+ *
1023
+ * But the operator's session is alive in the cookie, shared across
1024
+ * the zone and never touched. The second attempt goes without
1025
+ * `Authorization`, the cookie speaks, and they become themselves
1026
+ * again — which is what the person expected when ending it in any of
1027
+ * the tabs.
1028
+ *
1029
+ * One attempt only: if this one fails too, there is no session
1030
+ * behind it, and the path below (logging out) is correct.
1031
+ */
1032
+ if (rejected) {
1033
+ setStoredToken(null);
1034
+ try {
1035
+ const semToken = await getSession();
1036
+ if (semToken?.user) {
1037
+ /*
1038
+ * This path ALSO swaps the identity on screen.
1039
+ *
1040
+ * It is the one taken when the impersonation is ended in
1041
+ * another tab: the target's token is refused, the cookie
1042
+ * returns the operator — but the screen stays mounted
1043
+ * with the target's lists, which nobody will re-read.
1044
+ *
1045
+ * Same guard as the ordinary revalidation: mark the
1046
+ * switch to silence the in-flight 403s, and reload so
1047
+ * nothing of the previous identity survives.
1048
+ */
1049
+ const anterior = get().user;
1050
+ const trocou = anterior && anterior.id !== semToken.user.id;
1051
+ set({
1052
+ user: semToken.user,
1053
+ currentSession: semToken.session ?? null,
1054
+ impersonation: semToken.impersonation ?? null,
1055
+ loading: false
1056
+ });
1057
+ if (trocou && typeof window !== 'undefined') {
1058
+ markIdentitySwitching();
1059
+ window.location.reload();
1060
+ }
1061
+ return;
1062
+ }
1063
+ } catch {
1064
+ // Sem sessão atrás do token morto: segue para o caminho
1065
+ // normal e desloga, que é o comportamento correto.
1066
+ }
1067
+ }
1068
+
1069
+ // An unreachable server says nothing about an established session.
1070
+ // On a revalidation the user is already signed in: keep them, and
1071
+ // try again on the next focus.
1072
+ if (!rejected && !trusted) {
1073
+ console.warn('[AuthStore] Revalidação falhou, mantendo a sessão:', error);
1074
+ return;
1075
+ }
1076
+ console.error('Erro na inicialização:', error);
1077
+ set({
1078
+ user: null,
1079
+ currentSession: null,
1080
+ loading: false
1081
+ });
1082
+ }
1083
+ },
1084
+ /* Init ao montar o Provider */
1085
+ init: async () => {
1086
+ await get().syncSession({
1087
+ trusted: true
1088
+ });
1089
+ },
1090
+ /*
1091
+ * Re-reads the session when the tab regains focus.
1092
+ *
1093
+ * `init` runs once, so a tab opened BEFORE the user signed in elsewhere
1094
+ * stays on `user: null` until a manual refresh. The session cookie is
1095
+ * shared across the zone, and localStorage is not — it is per origin — so
1096
+ * the `storage` event never crosses between two dashboards. Asking the
1097
+ * server on focus is what carries a sign-in (and a sign-out) between them.
1098
+ *
1099
+ * Throttled: `focus` and `visibilitychange` fire together, and alt-tabbing
1100
+ * would otherwise turn into a burst of `/auth/session` calls.
1101
+ */
1102
+ revalidate: async ({
1103
+ force = false
1104
+ } = {}) => {
1105
+ const now = Date.now();
1106
+ // `force` skips the floor between reads: a caller on its own interval
1107
+ // (the impersonation poll) already controls the frequency, and the floor
1108
+ // exists for focus, which fires several times in a row when windows
1109
+ // change.
1110
+ if (!force && now - get().lastRevalidatedAt < REVALIDATE_THROTTLE_MS) return;
1111
+ set({
1112
+ lastRevalidatedAt: now
1113
+ });
1114
+ await get().syncSession({
1115
+ trusted: false
1116
+ });
1117
+ },
1118
+ /* Ações de Autenticação */
1119
+ /* Sign in — ask for the code and trade it for the session. No third step. */
1120
+
1121
+ requestCode: async (email, options) => {
1122
+ const {
1123
+ setLoading
1124
+ } = get();
1125
+ setLoading('requestCode', true);
1126
+ set({
1127
+ error: null
1128
+ });
1129
+ try {
1130
+ return await requestCode(email, options);
1131
+ } catch (err) {
1132
+ set({
1133
+ error: err
1134
+ });
1135
+ throw err;
1136
+ } finally {
1137
+ setLoading('requestCode', false);
1138
+ }
1139
+ },
1140
+ verifyCode: async (email, code) => {
1141
+ const {
1142
+ setLoading
1143
+ } = get();
1144
+ setLoading('verifyCode', true);
1145
+ set({
1146
+ error: null
1147
+ });
1148
+ try {
1149
+ const result = await verifyCode(email, code);
1150
+
1151
+ // The session comes with the response; without it the "this device"
1152
+ // badge cannot identify the current session.
1153
+ if (result.session) set({
1154
+ currentSession: result.session
1155
+ });
1156
+ set({
1157
+ user: result.user || null,
1158
+ loading: false
1159
+ });
1160
+ return result;
1161
+ } catch (err) {
1162
+ set({
1163
+ error: err
1164
+ });
1165
+ throw err;
1166
+ } finally {
1167
+ setLoading('verifyCode', false);
1168
+ }
1169
+ },
1170
+ signOut: async () => {
1171
+ const {
1172
+ setLoading
1173
+ } = get();
1174
+ setLoading('signOut', true);
1175
+ try {
1176
+ await signOut();
1177
+ set({
1178
+ user: null
1179
+ });
1180
+ // Sincronizar logout entre abas
1181
+ if (typeof window !== 'undefined') {
1182
+ window.localStorage.setItem('auth:logout', Date.now());
1183
+ }
1184
+ } finally {
1185
+ setLoading('signOut', false);
1186
+ }
1187
+ },
1188
+ /* Session */
1189
+ getSession: async () => {
1190
+ try {
1191
+ const sessionData = await getSession();
1192
+ // Store current session for comparison (includes id)
1193
+ if (sessionData?.session) {
1194
+ set({
1195
+ currentSession: sessionData.session
1196
+ });
1197
+ }
1198
+ return sessionData;
1199
+ } catch (err) {
1200
+ set({
1201
+ error: err
1202
+ });
1203
+ throw err;
1204
+ }
1205
+ },
1206
+ listSessions: async () => {
1207
+ const {
1208
+ setLoading
1209
+ } = get();
1210
+ setLoading('listSessions', true);
1211
+ set({
1212
+ error: null
1213
+ });
1214
+ try {
1215
+ const result = await listSessions();
1216
+ set({
1217
+ sessions: result || []
1218
+ });
1219
+ setLoading('listSessions', false);
1220
+ return result;
1221
+ } catch (err) {
1222
+ set({
1223
+ error: err,
1224
+ sessions: []
1225
+ });
1226
+ setLoading('listSessions', false);
1227
+ throw err;
1228
+ }
1229
+ },
1230
+ revokeSession: async sessionId => {
1231
+ const {
1232
+ setLoading,
1233
+ currentSession,
1234
+ sessions,
1235
+ signOut
1236
+ } = get();
1237
+ setLoading('revokeSession', sessionId);
1238
+ set({
1239
+ error: null
1240
+ });
1241
+ try {
1242
+ // Detect if current session OR last remaining session
1243
+ const isCurrent = sessionId === currentSession?.id;
1244
+ const isLast = sessions.length === 1 && sessions[0].id === sessionId;
1245
+
1246
+ // Se for a sessão atual ou a última, faz logout normal
1247
+ if (isCurrent || isLast) {
1248
+ await signOut();
1249
+ // signOut já limpa loading states e erros no finally, mas
1250
+ // como estamos dentro do fluxo deste método, garantimos:
1251
+ setLoading('revokeSession', null);
1252
+ return;
1253
+ }
1254
+ await revokeSession(sessionId);
1255
+
1256
+ // Remove a sessão revogada da lista local
1257
+ set(state => ({
1258
+ sessions: state.sessions.filter(s => s.id !== sessionId)
1259
+ }));
1260
+ setLoading('revokeSession', null);
1261
+ } catch (err) {
1262
+ set({
1263
+ error: err
1264
+ });
1265
+ setLoading('revokeSession', null);
1266
+ throw err;
1267
+ }
1268
+ },
1269
+ revokeOtherSessions: async () => {
1270
+ const {
1271
+ setLoading,
1272
+ listSessions
1273
+ } = get();
1274
+ setLoading('revokeSession', 'all');
1275
+ set({
1276
+ error: null
1277
+ });
1278
+ try {
1279
+ await revokeOtherSessions();
1280
+ // Refresh sessions list after revocation
1281
+ await listSessions();
1282
+ setLoading('revokeSession', null);
1283
+ } catch (err) {
1284
+ set({
1285
+ error: err
1286
+ });
1287
+ setLoading('revokeSession', null);
1288
+ throw err;
1289
+ }
1290
+ },
1291
+ /* Refresh do token em background */
1292
+ startRefresh: () => {
1293
+ if (typeof window === 'undefined') return;
1294
+ const refreshInterval = setInterval(async () => {
1295
+ try {
1296
+ // Se o usuário está autenticado mas o token está próximo do vencimento
1297
+ if (isAuthenticated()) {
1298
+ const token = window.localStorage.getItem('auth:token');
1299
+ if (token) {
1300
+ // Usa o decoder oficial do SDK que é mais seguro
1301
+ const payload = decodeJWT(token);
1302
+
1303
+ // Se não for um JWT ou não tiver expiração, não fazemos refresh em background
1304
+ // O backend cuidará da expiração da sessão opaca via 401 nas requisições normais
1305
+ if (!payload || !payload.exp) return;
1306
+ const now = Date.now() / 1000;
1307
+ const timeUntilExpiry = payload.exp - now;
1308
+
1309
+ // Se o token expira em menos de 5 minutos, tenta o refresh
1310
+ if (timeUntilExpiry < 300) {
1311
+ try {
1312
+ const refreshed = await refreshToken();
1313
+ const user = getCurrentUser();
1314
+ set({
1315
+ user
1316
+ });
1317
+ // O refresh rotaciona o token mantendo o mesmo id de sessão.
1318
+ if (refreshed?.session) set({
1319
+ currentSession: refreshed.session
1320
+ });
1321
+ } catch (refreshErr) {
1322
+ console.warn('[AuthStore] Falha ao renovar token:', refreshErr);
1323
+ // Só desloga se for um erro de autenticação explícito (401)
1324
+ if (refreshErr.res?.status === 401) {
1325
+ set({
1326
+ user: null
1327
+ });
1328
+ window.localStorage.removeItem('auth:token');
1329
+ }
1330
+ }
1331
+ }
1332
+ }
1333
+ }
1334
+ } catch (error) {
1335
+ // Erros de processamento interno não devem deslogar o usuário
1336
+ console.error('[AuthStore] Erro no ciclo de refresh automático:', error);
1337
+ }
1338
+ }, 4 * 60 * 1000); // Verifica a cada 4 minutos
1339
+
1340
+ // Limpa o intervalo quando necessário
1341
+ if (typeof window !== 'undefined') {
1342
+ window.addEventListener('beforeunload', () => {
1343
+ clearInterval(refreshInterval);
1344
+ });
1345
+ }
1346
+ },
1347
+ /* Verifica se o token ainda é válido */
1348
+ checkTokenValidity: () => {
1349
+ if (!isAuthenticated()) {
1350
+ set({
1351
+ user: null
1352
+ });
1353
+ return false;
1354
+ }
1355
+ return true;
1356
+ },
1357
+ /* Atualizar usuário manualmente */
1358
+ setUser: user => set({
1359
+ user
1360
+ }),
1361
+ /* Profile Management */
1362
+ updateProfile: async data => {
1363
+ const {
1364
+ setLoading
1365
+ } = get();
1366
+ setLoading('updateProfile', true);
1367
+ set({
1368
+ error: null
1369
+ });
1370
+ try {
1371
+ const result = await updateProfile(data);
1372
+ // Atualiza o user no store com os novos dados
1373
+ set(state => ({
1374
+ user: state.user ? {
1375
+ ...state.user,
1376
+ ...data
1377
+ } : null
1378
+ }));
1379
+ setLoading('updateProfile', false);
1380
+ return result;
1381
+ } catch (err) {
1382
+ set({
1383
+ error: err
1384
+ });
1385
+ setLoading('updateProfile', false);
1386
+ throw err;
1387
+ }
1388
+ }
1389
+ }));
1390
+
1391
+ function remaining(expiresAt) {
1392
+ if (!expiresAt) return null;
1393
+ const ms = new Date(expiresAt).getTime() - Date.now();
1394
+ if (ms <= 0) return null;
1395
+ const totalSeconds = Math.floor(ms / 1000);
1396
+ return `${Math.floor(totalSeconds / 60)}:${String(totalSeconds % 60).padStart(2, '0')}`;
1397
+ }
1398
+
1399
+ /*
1400
+ * A FLOATING PILL, NOT A STRIP AT THE TOP — and this is a layout decision, not
1401
+ * a cosmetic one.
1402
+ *
1403
+ * The bar used to be `position: sticky; top: 0`, rendered just above the panel's
1404
+ * own tree. That takes vertical space in the document flow, and all seven panels
1405
+ * lay themselves out with Mantine's `AppShell`, which positions its Header and
1406
+ * Navbar at offsets it computes from its OWN props (`header={{ height }}`, and
1407
+ * that height is responsive — 56 on mobile, 0 on desktop in some panels). The
1408
+ * strip pushed that whole construction down: the panel's header ended up under
1409
+ * the bar, and the sidebar got clipped.
1410
+ *
1411
+ * The SDK cannot fix that by measuring: it would have to know each panel's
1412
+ * header height, at every breakpoint, and stay in sync with seven layouts it
1413
+ * does not own. Any number it picks is wrong somewhere.
1414
+ *
1415
+ * So the bar stops competing for the top of the page. `position: fixed` with
1416
+ * `inset: auto 0 20px` takes it out of the flow entirely — it reserves no
1417
+ * space, displaces nothing, and floats over the content at the BOTTOM, where no
1418
+ * panel puts a fixed header. The visibility that the feature depends on is
1419
+ * preserved: it is always on screen, centred, and impossible to scroll away
1420
+ * from. The risk this component exists to cover is the operator FORGETTING they
1421
+ * are inside someone else's account, and a floating pill answers that as well
1422
+ * as a strip did — without breaking the screen underneath.
1423
+ *
1424
+ * `pointerEvents` is handled in two layers: the full-width wrapper lets clicks
1425
+ * through (it spans the viewport and would otherwise swallow a row of the UI),
1426
+ * and the pill itself takes them back so the button stays clickable.
1427
+ */
1428
+ const styles = {
1429
+ wrap: {
1430
+ position: 'fixed',
1431
+ left: 0,
1432
+ right: 0,
1433
+ bottom: 20,
1434
+ zIndex: 2147483647,
1435
+ display: 'flex',
1436
+ justifyContent: 'center',
1437
+ // The strip spans the viewport; without this it would eat clicks on
1438
+ // whatever sits behind it for the full width of the screen.
1439
+ pointerEvents: 'none',
1440
+ // Mobile: never let the pill run under the home indicator.
1441
+ paddingLeft: 'max(12px, env(safe-area-inset-left))',
1442
+ paddingRight: 'max(12px, env(safe-area-inset-right))'
1443
+ },
1444
+ bar: {
1445
+ display: 'flex',
1446
+ alignItems: 'center',
1447
+ justifyContent: 'center',
1448
+ flexWrap: 'wrap',
1449
+ gap: 10,
1450
+ padding: '10px 16px',
1451
+ borderRadius: 999,
1452
+ // The brand's black. It stands out by INVERSION, not by an alert
1453
+ // colour: red would say "something went wrong", and the state is
1454
+ // "you are borrowed".
1455
+ background: '#11181C',
1456
+ color: '#FFFFFF',
1457
+ fontSize: 13,
1458
+ fontFamily: 'inherit',
1459
+ lineHeight: 1.4,
1460
+ maxWidth: '100%',
1461
+ // Detaches the pill from the content it floats over, which a flat strip
1462
+ // did not need because it had an edge to sit against.
1463
+ boxShadow: '0 8px 24px rgba(0, 0, 0, 0.28)',
1464
+ pointerEvents: 'auto'
1465
+ },
1466
+ strong: {
1467
+ fontWeight: 700
1468
+ },
1469
+ clock: {
1470
+ fontVariantNumeric: 'tabular-nums',
1471
+ opacity: 0.75
1472
+ },
1473
+ button: {
1474
+ marginLeft: 4,
1475
+ padding: '4px 12px',
1476
+ border: 0,
1477
+ borderRadius: 999,
1478
+ background: '#FFFFFF',
1479
+ color: '#11181C',
1480
+ font: 'inherit',
1481
+ fontSize: 12,
1482
+ fontWeight: 700,
1483
+ cursor: 'pointer'
1484
+ }
1485
+ };
1486
+ function ImpersonationBanner() {
1487
+ const impersonation = useAuthStore(s => s.impersonation);
1488
+ const user = useAuthStore(s => s.user);
1489
+ const [left, setLeft] = react.useState(() => remaining(impersonation?.expiresAt));
1490
+ const [ending, setEnding] = react.useState(false);
1491
+
1492
+ /*
1493
+ * Encerrar recarrega a página.
1494
+ *
1495
+ * The whole panel is already mounted with the target's data — lists,
1496
+ * stores, caches. Swapping the identity underneath would leave half the
1497
+ * screen with one person's data and half with another's. Reloading is the
1498
+ * simplest way to make everything come back coherent, and it happens once,
1499
+ * at the end of the session.
1500
+ */
1501
+ /*
1502
+ * O vencimento, sem clique.
1503
+ *
1504
+ * It does not call `/impersonation/:id/end`: the session is already gone,
1505
+ * and the route would answer 403 ("belongs to another session") because the
1506
+ * presented token died. What is left to do is local — drop the target's
1507
+ * token — and let the server say who the person is on the next load.
1508
+ *
1509
+ * The impersonation cookie needs no explicit clearing: it was issued with a
1510
+ * `Max-Age` equal to the session TTL, so the browser discards it on its own.
1511
+ * And even if it lingered a moment, `requireAuth` tries the candidates in
1512
+ * order and falls through to the operator's session.
1513
+ */
1514
+ const endedByTimer = react.useCallback(() => {
1515
+ markIdentitySwitching();
1516
+ setStoredToken(null);
1517
+ window.location.assign('/');
1518
+ }, []);
1519
+ const handleEnd = async () => {
1520
+ if (!impersonation?.id) return;
1521
+ setEnding(true);
1522
+ markIdentitySwitching();
1523
+ try {
1524
+ await endImpersonation(impersonation.id);
1525
+
1526
+ /*
1527
+ * The TARGET's token is in localStorage — the SDK wrote it there
1528
+ * so the other products would send the right header. Ending without
1529
+ * dropping it would leave the panel presenting itself as the target
1530
+ * in every product that reads that key, even with the cookie gone.
1531
+ *
1532
+ * The reload below asks for the session again, and the operator's
1533
+ * session cookie (never touched) rewrites the correct value.
1534
+ */
1535
+ setStoredToken(null);
1536
+ } catch {
1537
+ // If it fails, the session expires on its own within minutes.
1538
+ // Reloading anyway avoids leaving the operator stuck in a bar that
1539
+ // does not answer.
1540
+ }
1541
+ /*
1542
+ * Goes to the panel's ROOT, instead of reloading the current URL.
1543
+ *
1544
+ * The URL where an impersonation ends usually belongs to the TARGET — a
1545
+ * project of theirs in Hoster, an application of theirs in Auth.
1546
+ * Reloading there returns the operator to a route that may not be
1547
+ * theirs, and the screen answers 403 or sits in a skeleton forever.
1548
+ *
1549
+ * `/` is the home of every panel in the suite, and the only route that
1550
+ * answers for any identity. Each product decides what it means.
1551
+ */
1552
+ window.location.assign('/');
1553
+ };
1554
+ react.useEffect(() => {
1555
+ if (!impersonation) return;
1556
+ const timer = setInterval(() => {
1557
+ const restante = remaining(impersonation.expiresAt);
1558
+ setLeft(restante);
1559
+
1560
+ /*
1561
+ * Vencido: a tela tem que acompanhar, e sem ninguém clicar em nada.
1562
+ *
1563
+ * Until now the clock hit zero and nothing else happened: the bar
1564
+ * stayed on screen, `localStorage` kept the target's dead token, and
1565
+ * every product answered 401 — which panels read as the end of a
1566
+ * session and handle by signing out. The operator lost their own
1567
+ * account because a borrowed session expired.
1568
+ *
1569
+ * `endedByTimer` walks the same path as the button: drop the
1570
+ * target's token and go to the root. What decides who shows up next
1571
+ * is the SERVER — the operator's session cookie was never touched,
1572
+ * so if it is alive the home loads as them; if there is no session
1573
+ * behind it, `/auth/session` answers without a user and the panel
1574
+ * sends them to the login, which is correct. There is nothing to
1575
+ * guess here.
1576
+ */
1577
+ if (!restante) endedByTimer();
1578
+ }, 1000);
1579
+ return () => clearInterval(timer);
1580
+ }, [impersonation, endedByTimer]);
1581
+ if (!impersonation) return null;
1582
+
1583
+ // Durante o primeiro segundo `left` ainda é o valor de outra impersonação
1584
+ // (ou nulo); derivar aqui evita um setState no corpo do efeito só para isso.
1585
+ const clock = left ?? remaining(impersonation.expiresAt);
1586
+ return /*#__PURE__*/jsxRuntime.jsx("div", {
1587
+ style: styles.wrap,
1588
+ children: /*#__PURE__*/jsxRuntime.jsxs("div", {
1589
+ style: styles.bar,
1590
+ role: "status",
1591
+ children: [/*#__PURE__*/jsxRuntime.jsxs("span", {
1592
+ children: ["Voc\xEA est\xE1 vendo como ", /*#__PURE__*/jsxRuntime.jsx("span", {
1593
+ style: styles.strong,
1594
+ children: user?.name || user?.email
1595
+ }), impersonation.actor ? /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
1596
+ children: [" \xB7 sess\xE3o aberta por ", impersonation.actor]
1597
+ }) : null]
1598
+ }), clock ? /*#__PURE__*/jsxRuntime.jsxs("span", {
1599
+ style: styles.clock,
1600
+ children: ["encerra em ", clock]
1601
+ }) : null, impersonation.id ? /*#__PURE__*/jsxRuntime.jsx("button", {
1602
+ type: "button",
1603
+ style: styles.button,
1604
+ onClick: handleEnd,
1605
+ disabled: ending,
1606
+ children: ending ? 'Encerrando…' : 'Encerrar'
1607
+ }) : null]
1608
+ })
1609
+ });
1610
+ }
1611
+
1612
+ const AuthContext = /*#__PURE__*/react.createContext(); // só para ter o Provider em JSX
1613
+
1614
+ // How often a tab asks the server who it is. Short enough for an identity
1615
+ // switch to surface on its own, long enough not to weigh on an idle panel.
1616
+ const IMPERSONATION_POLL_MS = 10 * 1000;
1617
+ function AuthProvider({
1618
+ children,
1619
+ apiKey,
1620
+ // API Key para header X-API-Key (obrigatória exceto em modo internal)
1621
+ apiUrl,
1622
+ // URL do manager (opcional, padrão: http://auth.worker.myinfrastructure.click)
1623
+ internal = false,
1624
+ // Modo interno: não exige API Key (para aplicações same-domain como dashboard)
1625
+ onError // Callback de erro global
1626
+ }) {
1627
+ // Validação de props obrigatórias
1628
+ // apiKey só é obrigatória se não estiver em modo internal
1629
+ if (!internal && !apiKey) {
1630
+ throw new Error('[@ciromaciel/auth-react] apiKey é obrigatória no AuthProvider. ' + 'Obtenha sua API Key no dashboard em https://auth.dashboard.myinfrastructure.click');
1631
+ }
1632
+ const init = useAuthStore(s => s.init);
1633
+ const startRefresh = useAuthStore(s => s.startRefresh);
1634
+ const revalidate = useAuthStore(s => s.revalidate);
1635
+ const checkTokenValidity = useAuthStore(s => s.checkTokenValidity);
1636
+
1637
+ // Configura SDK com apiKey, apiUrl e modo interno
1638
+ // Usamos useMemo para garantir que a configuração ocorra ANTES dos efeitos dos componentes filhos
1639
+ react.useMemo(() => {
1640
+ configure({
1641
+ apiKey,
1642
+ apiUrl,
1643
+ internal
1644
+ });
1645
+
1646
+ /*
1647
+ * A social sign-in landing here brings its token in the fragment.
1648
+ *
1649
+ * It is consumed in this `useMemo`, BEFORE `init()` runs in the effect
1650
+ * below: `init` reads the stored token to resolve the session, so a
1651
+ * token still sitting in the URL at that moment would be missed, and the
1652
+ * person would land signed out on the page they just signed into.
1653
+ *
1654
+ * Reading it also strips it from the address bar, so a copied URL never
1655
+ * carries a live session.
1656
+ */
1657
+ consumeSocialToken();
1658
+ }, [apiKey, apiUrl, internal]);
1659
+ react.useEffect(() => {
1660
+ init();
1661
+ startRefresh();
1662
+ }, [init, startRefresh]);
1663
+
1664
+ // Sincronização entre abas - escuta logout e mudanças no token
1665
+ react.useEffect(() => {
1666
+ if (typeof window === 'undefined') return;
1667
+ const handleStorageChange = event => {
1668
+ if (event.key === 'auth:logout') {
1669
+ // Limpa o user do store - a aplicação redireciona automaticamente quando user é null
1670
+ useAuthStore.setState({
1671
+ user: null,
1672
+ currentSession: null,
1673
+ sessions: []
1674
+ });
1675
+ }
1676
+ /*
1677
+ * Another tab of this origin switched identity.
1678
+ *
1679
+ * It clears its own storage and reloads, but each tab has its own
1680
+ * mounted screens — lists and counters that belong to whoever was
1681
+ * signed in a moment ago. Wiping here and reloading is what makes a
1682
+ * sibling tab follow along instead of showing the previous person's
1683
+ * data under the new person's name.
1684
+ *
1685
+ * This is the SDK's job, not each app's: every consumer of
1686
+ * `AuthProvider` gets it without writing anything, ours and our
1687
+ * customers' alike.
1688
+ */
1689
+ if (event.key === 'auth:identity-switched') {
1690
+ markIdentitySwitching();
1691
+ window.location.assign('/');
1692
+ return;
1693
+ }
1694
+
1695
+ // The token changed in another tab of THIS origin.
1696
+ //
1697
+ // `checkTokenValidity` only reads the stored JWT's `exp`, which
1698
+ // cannot tell a different person from the same one — and after an
1699
+ // impersonation starts or ends, a different person is exactly what
1700
+ // it is. Asking the server is what makes the sibling tab follow the
1701
+ // switch instead of sitting on the previous identity's data.
1702
+ //
1703
+ // `force` skips the focus throttle: this is an event, not a stream,
1704
+ // and a background tab may never get focus to catch up.
1705
+ if (event.key === 'auth:token') {
1706
+ revalidate({
1707
+ force: true
1708
+ });
1709
+ }
1710
+ };
1711
+
1712
+ // Escuta evento de sessão revogada (quando o usuário revoga sua própria sessão)
1713
+ const handleSessionRevoked = () => {
1714
+ // Limpa o usuário do store
1715
+ useAuthStore.setState({
1716
+ user: null,
1717
+ currentSession: null,
1718
+ sessions: []
1719
+ });
1720
+ // Dispara evento de logout para sincronizar entre abas
1721
+ localStorage.setItem('auth:logout', Date.now());
1722
+ };
1723
+ window.addEventListener('storage', handleStorageChange);
1724
+ window.addEventListener('auth:session-revoked', handleSessionRevoked);
1725
+ return () => {
1726
+ window.removeEventListener('storage', handleStorageChange);
1727
+ window.removeEventListener('auth:session-revoked', handleSessionRevoked);
1728
+ };
1729
+ }, [revalidate]);
1730
+
1731
+ // Revalidate the session when the tab regains focus.
1732
+ //
1733
+ // `init` runs once: a tab opened BEFORE the user signed in on another
1734
+ // dashboard stays on `user: null` until a manual refresh. The session
1735
+ // cookie belongs to the zone, but localStorage is per origin — so the
1736
+ // `storage` event never crosses from one dashboard to another. Asking the
1737
+ // server on focus is what carries a sign-in (and a sign-out) between them.
1738
+ react.useEffect(() => {
1739
+ if (typeof window === 'undefined') return;
1740
+ const handleFocus = () => {
1741
+ if (document.visibilityState === 'visible') revalidate();
1742
+ };
1743
+ document.addEventListener('visibilitychange', handleFocus);
1744
+ window.addEventListener('focus', handleFocus);
1745
+ return () => {
1746
+ document.removeEventListener('visibilitychange', handleFocus);
1747
+ window.removeEventListener('focus', handleFocus);
1748
+ };
1749
+ }, [revalidate]);
1750
+
1751
+ // Verifica validade do token periodicamente
1752
+ react.useEffect(() => {
1753
+ if (typeof window === 'undefined') return;
1754
+ const interval = setInterval(() => {
1755
+ checkTokenValidity();
1756
+ }, 30 * 1000); // Verifica a cada 30 segundos
1757
+
1758
+ return () => clearInterval(interval);
1759
+ }, [checkTokenValidity]);
1760
+
1761
+ // Ask the server every so often — even with the tab in the background, and
1762
+ // WITHOUT depending on knowing an impersonation is open.
1763
+ //
1764
+ // `checkTokenValidity` above only reads the `exp` of the stored JWT, and a
1765
+ // target's token has an `exp` in the future: it stays "valid" after the
1766
+ // impersonation ended. Focus revalidation does not help a tab nobody
1767
+ // touched. That is what left Functions showing the target after ending
1768
+ // elsewhere: nothing in that tab had a reason to ask again.
1769
+ //
1770
+ // Gating this on `impersonation` was the obvious move and the wrong one: it
1771
+ // makes the recovery depend on the very state that goes stale. When the
1772
+ // store lost `impersonation` while still holding the target as `user` — the
1773
+ // bar disappeared and the data did not — the poll stopped with it, and the
1774
+ // tab had no way back at all.
1775
+ //
1776
+ // Unconditional, the tab always has a way back. One request every ten
1777
+ // seconds against a route that answers from the session is cheap; being
1778
+ // stuck as another person is not.
1779
+ react.useEffect(() => {
1780
+ if (typeof window === 'undefined') return;
1781
+ const interval = setInterval(() => revalidate({
1782
+ force: true
1783
+ }), IMPERSONATION_POLL_MS);
1784
+ return () => clearInterval(interval);
1785
+ }, [revalidate]);
1786
+
1787
+ // Contexto com onError callback
1788
+ const contextValue = react.useMemo(() => ({
1789
+ onError
1790
+ }), [onError]);
1791
+
1792
+ /*
1793
+ * The impersonation bar ships WITH the Provider.
1794
+ *
1795
+ * That is what makes all seven panels warn without any of them being
1796
+ * changed: they all already wrap the app in `AuthProvider`. Letting each
1797
+ * panel mount it by hand is how one of them gets left out — and the panel
1798
+ * left out is precisely where the operator forgets whose account they are
1799
+ * in.
1800
+ *
1801
+ * It renders nothing when no impersonation is open, so it costs nothing in
1802
+ * the normal case.
1803
+ */
1804
+ return /*#__PURE__*/jsxRuntime.jsxs(AuthContext.Provider, {
1805
+ value: contextValue,
1806
+ children: [/*#__PURE__*/jsxRuntime.jsx(ImpersonationBanner, {}), children]
1807
+ });
1808
+ }
1809
+
1810
+ /* Hooks "facade" que a app vai usar */
1811
+ const useAuth = () => useAuthStore(shallow.useShallow(s => ({
1812
+ user: s.user,
1813
+ loading: s.loading,
1814
+ error: s.error,
1815
+ isAuthenticated: s.user !== null,
1816
+ requestCode: s.requestCode,
1817
+ verifyCode: s.verifyCode,
1818
+ signOut: s.signOut
1819
+ })));
1820
+
1821
+ /**
1822
+ * Sign in: the two steps, and nothing else.
1823
+ *
1824
+ * `requestCode(email, { name })` sends the code. `verifyCode(email, code)`
1825
+ * returns the session. There is no separate sign-up — the first entry creates
1826
+ * the account.
1827
+ */
1828
+ const useSignIn = () => useAuthStore(shallow.useShallow(s => ({
1829
+ requestCode: s.requestCode,
1830
+ verifyCode: s.verifyCode,
1831
+ sending: s.loadingStates.requestCode,
1832
+ verifying: s.loadingStates.verifyCode,
1833
+ error: s.error
1834
+ })));
1835
+
1836
+ // Auth Actions
1837
+ const useSignOut = () => useAuthStore(s => s.signOut);
1838
+ const useCheckToken = () => useAuthStore(s => s.checkTokenValidity);
1839
+
1840
+ // Session Hook
1841
+ const useSession = () => useAuthStore(shallow.useShallow(s => ({
1842
+ getSession: s.getSession,
1843
+ user: s.user,
1844
+ setUser: s.setUser
1845
+ })));
1846
+
1847
+ // Loading States Hook
1848
+ const useAuthLoading = () => useAuthStore(s => s.loadingStates);
1849
+
1850
+ // Profile Management Hook (novo nome estilo Clerk)
1851
+ const useUser = () => useAuthStore(shallow.useShallow(s => ({
1852
+ user: s.user,
1853
+ updateProfile: s.updateProfile,
1854
+ loadingUpdateProfile: s.loadingStates.updateProfile,
1855
+ error: s.error
1856
+ })));
1857
+
1858
+ // Sessions Management Hook
1859
+ const useSessions = () => useAuthStore(shallow.useShallow(s => ({
1860
+ currentSession: s.currentSession,
1861
+ sessions: s.sessions,
1862
+ getSession: s.getSession,
1863
+ listSessions: s.listSessions,
1864
+ revokeSession: s.revokeSession,
1865
+ revokeOtherSessions: s.revokeOtherSessions,
1866
+ loadingListSessions: s.loadingStates.listSessions,
1867
+ loadingRevokeSession: s.loadingStates.revokeSession,
1868
+ error: s.error
1869
+ })));
1870
+
1871
+ /**
1872
+ * The impersonation in progress, or `null`.
1873
+ *
1874
+ * Exposed so a panel can adapt what it OFFERS while the session is borrowed.
1875
+ * The banner already says whose account this is; this is for the screens that
1876
+ * would otherwise invite the target to do something only an administrator does
1877
+ * — "create your first company" makes no sense for an end user being looked at.
1878
+ *
1879
+ * Returns the record (`{ id, actor, expiresAt }`), so a screen can name who
1880
+ * opened the session without a second call.
1881
+ */
1882
+ const useImpersonation = () => useAuthStore(s => s.impersonation);
1883
+
1884
+ // Application Logo Hook
1885
+ const useApplicationLogo = () => {
1886
+ const applicationInfo = useAuthStore(s => s.applicationInfo);
1887
+ // Retorna o logo da aplicação ou null (componentes usam fallback padrão)
1888
+ return applicationInfo?.image || null;
1889
+ };
1890
+
1891
+ function Protect({
1892
+ fallback = /*#__PURE__*/jsxRuntime.jsx("p", {
1893
+ children: "\u231B Carregando..."
1894
+ }),
1895
+ redirectTo = '/login'
1896
+ }) {
1897
+ const {
1898
+ user,
1899
+ loading
1900
+ } = useAuth();
1901
+ if (loading) return fallback;
1902
+ if (!user) return /*#__PURE__*/jsxRuntime.jsx(reactRouterDom.Navigate, {
1903
+ to: redirectTo,
1904
+ replace: true
1905
+ });
1906
+ return /*#__PURE__*/jsxRuntime.jsx(reactRouterDom.Outlet, {});
1907
+ }
1908
+
1909
+ function GuestOnly({
1910
+ children,
1911
+ fallback = null,
1912
+ redirectTo = '/',
1913
+ extraOrigins = []
1914
+ }) {
1915
+ const {
1916
+ user,
1917
+ loading
1918
+ } = useAuth();
1919
+ if (loading) return fallback;
1920
+ if (!user) return children ?? /*#__PURE__*/jsxRuntime.jsx(reactRouterDom.Outlet, {});
1921
+ const to = getRedirectFromLocation(extraOrigins) || redirectTo;
1922
+
1923
+ // `//alvo` parece caminho e sai do domínio: é externo, e o roteador desta
1924
+ // aplicação não o alcança.
1925
+ const isInternal = to.startsWith('/') && !to.startsWith('//');
1926
+ if (!isInternal) {
1927
+ window.location.replace(to);
1928
+ return fallback;
1929
+ }
1930
+ return /*#__PURE__*/jsxRuntime.jsx(reactRouterDom.Navigate, {
1931
+ to: to,
1932
+ replace: true
1933
+ });
1934
+ }
1935
+
1936
+ function AuthCard({
1937
+ children,
1938
+ title,
1939
+ subtitle,
1940
+ logo,
1941
+ logoWidth = 133,
1942
+ width = 350,
1943
+ // Variant props
1944
+ variant = 'card',
1945
+ opened,
1946
+ onClose,
1947
+ modalProps = {},
1948
+ ...props
1949
+ }) {
1950
+ // Conteúdo interno compartilhado entre Card e Modal
1951
+ const content = /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
1952
+ gap: "sm",
1953
+ children: [(logo || title || subtitle) && /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
1954
+ gap: 6,
1955
+ align: "center",
1956
+ ta: "center",
1957
+ children: [logo && (typeof logo === 'string' ? /*#__PURE__*/jsxRuntime.jsx(core.Image, {
1958
+ src: logo,
1959
+ alt: "Auth",
1960
+ mx: "auto",
1961
+ w: logoWidth,
1962
+ fit: "contain"
1963
+ }) : logo), title && /*#__PURE__*/jsxRuntime.jsx(core.Title, {
1964
+ order: 3,
1965
+ ta: "center",
1966
+ children: title
1967
+ }), subtitle && /*#__PURE__*/jsxRuntime.jsx(core.Text, {
1968
+ size: "sm",
1969
+ c: "dimmed",
1970
+ ta: "center",
1971
+ children: subtitle
1972
+ })]
1973
+ }), children]
1974
+ });
1975
+
1976
+ // Renderizar como Modal
1977
+ if (variant === 'modal') {
1978
+ return /*#__PURE__*/jsxRuntime.jsx(core.Modal, {
1979
+ opened: opened,
1980
+ onClose: onClose,
1981
+ size: width + 50,
1982
+ withCloseButton: true,
1983
+ radius: 0,
1984
+ overlayProps: {
1985
+ backgroundOpacity: 0.55,
1986
+ blur: 3
1987
+ },
1988
+ title: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
1989
+ gap: "sm",
1990
+ wrap: "nowrap",
1991
+ children: [logo && (typeof logo === 'string' ? /*#__PURE__*/jsxRuntime.jsx(core.Image, {
1992
+ src: logo,
1993
+ alt: "Auth",
1994
+ h: logoWidth,
1995
+ fit: "contain"
1996
+ }) : logo), title && /*#__PURE__*/jsxRuntime.jsx(core.Title, {
1997
+ order: 4,
1998
+ children: title
1999
+ })]
2000
+ }),
2001
+ ...modalProps,
2002
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
2003
+ gap: "sm",
2004
+ children: [subtitle && /*#__PURE__*/jsxRuntime.jsx(core.Text, {
2005
+ size: "sm",
2006
+ c: "dimmed",
2007
+ children: subtitle
2008
+ }), children]
2009
+ })
2010
+ });
2011
+ }
2012
+
2013
+ // Renderizar como Card (default)
2014
+ return /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
2015
+ withBorder: true,
2016
+ shadow: "none",
2017
+ p: 24
2018
+ /*
2019
+ * `w` fixo nao encolhe: num celular de 375px o cartao de 350px
2020
+ * mais o padding da pagina estourava a tela e a coluna da direita
2021
+ * ficava cortada. `maw` mantem a MESMA largura onde ela cabe e
2022
+ * cede onde nao cabe.
2023
+ */,
2024
+ w: "100%",
2025
+ maw: width,
2026
+ radius: 0,
2027
+ ...props,
2028
+ children: content
2029
+ });
2030
+ }
2031
+
2032
+ const MARKS = {
2033
+ google: iconsReact.IconBrandGoogle
2034
+ };
2035
+
2036
+ /**
2037
+ * The social sign-in buttons, driven by what the APPLICATION has enabled.
2038
+ *
2039
+ * WHY THE LIST IS FETCHED AND NOT PASSED IN
2040
+ *
2041
+ * The owner configures Google in the Auth panel. Asking the integrator to ALSO
2042
+ * declare it in code would be two sources for one fact, and they would drift:
2043
+ * turning the provider off in the panel would leave a button that fails. So the
2044
+ * component asks the worker which providers are live.
2045
+ *
2046
+ * It renders NOTHING while it does not know, and nothing when the answer is
2047
+ * empty — an application with no social provider sees no divider, no gap, no
2048
+ * trace of this component.
2049
+ */
2050
+ function SocialButtons({
2051
+ labels = {},
2052
+ redirect,
2053
+ disabled = false
2054
+ }) {
2055
+ const [providers, setProviders] = react.useState(null);
2056
+ const [leaving, setLeaving] = react.useState(null);
2057
+ react.useEffect(() => {
2058
+ let active = true;
2059
+ getSocialProviders().then(list => {
2060
+ // The screen may have unmounted while the request was in flight.
2061
+ if (active) setProviders(list);
2062
+ });
2063
+ return () => {
2064
+ active = false;
2065
+ };
2066
+ }, []);
2067
+
2068
+ // `null` is "still asking", `[]` is "asked, and there are none". Both render
2069
+ // nothing, but only the second is a final answer.
2070
+ if (!providers || providers.length === 0) return null;
2071
+ return /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
2072
+ gap: "md",
2073
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Divider, {
2074
+ label: labels.socialDivider || 'ou',
2075
+ labelPosition: "center"
2076
+ }), providers.map(provider => {
2077
+ const Mark = MARKS[provider.provider];
2078
+ return /*#__PURE__*/jsxRuntime.jsx(core.Button, {
2079
+ variant: "default",
2080
+ fullWidth: true,
2081
+ size: "md"
2082
+ /*
2083
+ * `aria-disabled`, not `disabled`: Mantine's `disabled`
2084
+ * repaints the button grey and hides its label, and this
2085
+ * button is what the person is trying to click. The
2086
+ * handler below is what actually refuses.
2087
+ */,
2088
+ "aria-disabled": disabled || leaving !== null,
2089
+ loading: leaving === provider.provider,
2090
+ onClick: () => {
2091
+ if (disabled || leaving !== null) return;
2092
+ // Kept in state so a double click does not fire two
2093
+ // navigations — the second would orphan the first
2094
+ // `oauth_states` row.
2095
+ setLeaving(provider.provider);
2096
+ startSocialSignIn(provider.provider, {
2097
+ redirect
2098
+ });
2099
+ },
2100
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2101
+ gap: 10,
2102
+ wrap: "nowrap",
2103
+ justify: "center",
2104
+ children: [Mark && /*#__PURE__*/jsxRuntime.jsx(Mark, {
2105
+ size: 16,
2106
+ stroke: 1.5
2107
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
2108
+ fz: 14,
2109
+ fw: 700,
2110
+ children: labels.socialButton ? labels.socialButton(provider.name) : `Entrar com ${provider.name}`
2111
+ })]
2112
+ })
2113
+ }, provider.provider);
2114
+ })]
2115
+ });
2116
+ }
2117
+
2118
+ function Wordmark({
2119
+ fz = 11,
2120
+ c = 'gray.4',
2121
+ ...props
2122
+ }) {
2123
+ return /*#__PURE__*/jsxRuntime.jsx(core.Text, {
2124
+ component: "span",
2125
+ display: "block",
2126
+ ta: "center",
2127
+ fz: fz,
2128
+ fw: 800,
2129
+ lh: 1,
2130
+ tt: "uppercase",
2131
+ lts: "1.5px",
2132
+ c: c,
2133
+ ...props,
2134
+ children: "Auth"
2135
+ });
2136
+ }
2137
+
2138
+ const TERMS_URL = 'https://myinfrastructure.click/legal/terms';
2139
+
2140
+ /**
2141
+ * The terms notice under the first step.
2142
+ *
2143
+ * It sits on step ONE and nowhere else: the account is born on the first
2144
+ * `verify` and there is no separate sign-up screen to put it on — this form IS
2145
+ * the sign-up for whoever has never entered. On step two the person already
2146
+ * agreed by asking for the code; repeating it there would only push the code
2147
+ * field down.
2148
+ */
2149
+ function TermsNotice({
2150
+ url,
2151
+ text,
2152
+ linkText
2153
+ }) {
2154
+ if (!url) return null;
2155
+ return /*#__PURE__*/jsxRuntime.jsxs(core.Text, {
2156
+ size: "xs",
2157
+ c: "dimmed",
2158
+ ta: "center"
2159
+ /*
2160
+ * Tighter than the `gap="md"` of the Stack around it, and a hair
2161
+ * tighter than default leading. This is fine print under the action,
2162
+ * not a third step of the form — spaced like the field and the
2163
+ * button it would read as one more thing to do.
2164
+ */,
2165
+ mt: -4,
2166
+ lh: 1.4
2167
+ /*
2168
+ * Two short lines instead of one full-width line plus an orphan.
2169
+ * At the card's 350px the sentence wrapped as "…todos os / nossos
2170
+ * termos e condições", splitting the phrase away from the words that
2171
+ * govern it. Balanced, the break falls where the meaning does.
2172
+ */,
2173
+ style: {
2174
+ textWrap: 'balance'
2175
+ },
2176
+ children: [text, ' ', /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
2177
+ href: url,
2178
+ target: "_blank"
2179
+ /*
2180
+ * `noopener noreferrer` with `target="_blank"`: the same pair the
2181
+ * footers of the house use for anything leaving the origin
2182
+ * (`site-chrome.jsx`, `ProductLayout.jsx`). Opening in a new tab
2183
+ * is not decoration here — the person is mid-sign-in, and
2184
+ * navigating away would throw the typed email out.
2185
+ */,
2186
+ rel: "noopener noreferrer"
2187
+ /*
2188
+ * `inherit` takes the size and weight of the sentence around it,
2189
+ * so the link does not turn into a bold island inside the fine
2190
+ * print — but it also takes the COLOR, and in the Zen palette
2191
+ * `primaryColor: 'gray'` paints the Anchor near-black. Against
2192
+ * dimmed text that reads as emphasis, not as something to click.
2193
+ *
2194
+ * The underline is what says "link" here: it survives whatever
2195
+ * `primaryColor` each panel sets, and it is the only affordance
2196
+ * left once the colour matches the sentence.
2197
+ */,
2198
+ inherit: true,
2199
+ c: "inherit",
2200
+ underline: "always",
2201
+ children: linkText
2202
+ }), "."]
2203
+ });
2204
+ }
2205
+
2206
+ // The OAuth flow's pass-through screen.
2207
+ //
2208
+ // The panel is not the destination here: the user is authorizing an MCP client
2209
+ // and only passes through this origin because it is where the token lives. A
2210
+ // blank screen during the detour looks like a freeze; this one says what is
2211
+ // happening.
2212
+ function AuthTransition({
2213
+ label = 'Conectando…'
2214
+ }) {
2215
+ return /*#__PURE__*/jsxRuntime.jsx(core.Center, {
2216
+ style: {
2217
+ minHeight: '60vh'
2218
+ },
2219
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
2220
+ align: "center",
2221
+ gap: "sm",
2222
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Loader, {
2223
+ size: "xs"
2224
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
2225
+ size: "sm",
2226
+ c: "dimmed",
2227
+ children: label
2228
+ })]
2229
+ })
2230
+ });
2231
+ }
2232
+
2233
+ /**
2234
+ * Sign in — the only path there is.
2235
+ *
2236
+ * Two steps: the email receives a code, the code becomes a session. There is no
2237
+ * password, magic link, sign-up or recovery — first-time arrivals and returning
2238
+ * people walk exactly this screen, and the account is born on the first entry.
2239
+ *
2240
+ * Nothing here builds a callback URL. The code is typed in this very tab, so
2241
+ * there is no destination to validate — which is why the family of
2242
+ * origin/redirect bugs does not reach this flow.
2243
+ *
2244
+ * @param {object} props
2245
+ * @param {'card'|'modal'} [props.variant='card'] - Rendering mode
2246
+ * @param {boolean} [props.opened] - Visibility (only for variant="modal")
2247
+ * @param {function} [props.onClose] - Close callback (only for variant="modal")
2248
+ */
2249
+ function SignIn({
2250
+ // Configuration
2251
+ logo,
2252
+ // No default: it is computed below
2253
+ logoWidth = 133,
2254
+ title = 'Entrar',
2255
+ subtitle = 'Enviaremos um código para o seu e-mail',
2256
+ // Variant
2257
+ variant = 'card',
2258
+ opened,
2259
+ onClose,
2260
+ modalProps = {},
2261
+ // Where to send someone who ALREADY has a session and lands on this screen.
2262
+ // The landings link straight to /auth/signin without knowing a session
2263
+ // exists; without this, a signed-in user faced the form again. `null`
2264
+ // disables the detour.
2265
+ authenticatedRedirect = '/',
2266
+ // Shown while the session is being resolved and during the detour of an
2267
+ // already authenticated person. Avoids flashing the form to someone who
2268
+ // should never see it.
2269
+ redirectingFallback = null,
2270
+ // Callbacks
2271
+ onSuccess,
2272
+ // Post-sign-in redirect: by default the component honours `?redirect=` from
2273
+ // the URL, validating against the API's origin and the current one.
2274
+ // `redirectOrigins` adds domains; `handleRedirect={false}` hands control back
2275
+ // to the app.
2276
+ handleRedirect = true,
2277
+ redirectOrigins = [],
2278
+ onError,
2279
+ onCodeSent,
2280
+ // Custom labels
2281
+ labels = {},
2282
+ // The terms notice under the first step. `null` removes it — an internal
2283
+ // panel behind a VPN has no one to present terms to.
2284
+ termsUrl = TERMS_URL,
2285
+ /*
2286
+ * Social sign-in buttons.
2287
+ *
2288
+ * `'auto'` — the default — shows whatever the APPLICATION enabled in the
2289
+ * Auth panel, and nothing at all when it enabled none. It is additive: an
2290
+ * application with no provider configured renders exactly what it rendered
2291
+ * before this prop existed, so the default does not break anyone.
2292
+ *
2293
+ * `false` opts out entirely, for a screen that wants the emailed code only.
2294
+ */
2295
+ socialLogin = 'auto',
2296
+ ...cardProps
2297
+ }) {
2298
+ const user = useAuthStore(s => s.user);
2299
+ const authLoading = useAuthStore(s => s.loading);
2300
+ const requestCode = useAuthStore(s => s.requestCode);
2301
+ const verifyCode = useAuthStore(s => s.verifyCode);
2302
+ const sending = useAuthStore(s => s.loadingStates.requestCode);
2303
+ const verifying = useAuthStore(s => s.loadingStates.verifyCode);
2304
+
2305
+ // Which step we are on. The email is kept because the second step has to
2306
+ // present it again: it is the (email, code) pair the server validates.
2307
+ const [sentTo, setSentTo] = react.useState(null);
2308
+ const [code, setCode] = react.useState('');
2309
+ const [codeError, setCodeError] = react.useState(null);
2310
+
2311
+ // Hook that fetches the application's logo
2312
+ const applicationLogo = useApplicationLogo();
2313
+ const finalLogo = logo || applicationLogo || /*#__PURE__*/jsxRuntime.jsx(Wordmark, {});
2314
+ const navigate = reactRouterDom.useNavigate();
2315
+ const form$1 = form.useForm({
2316
+ initialValues: {
2317
+ email: ''
2318
+ },
2319
+ validate: {
2320
+ email: value => /^\S+@\S+$/.test(value) ? null : labels.invalidEmail || 'Email inválido'
2321
+ }
2322
+ });
2323
+
2324
+ // Whoever already has a session must not see the form. The landings point
2325
+ // to /auth/signin unconditionally, and since the session became shared
2326
+ // across `.myinfrastructure.click` (domain cookie) it is common to arrive here
2327
+ // already authenticated — the panel used to learn that only after a
2328
+ // redundant sign-in.
2329
+ //
2330
+ // It waits for `authLoading`: the provider's `init` resolves the session
2331
+ // asynchronously (cookie → /auth/session). Deciding before that would flash
2332
+ // the form to someone already signed in, or worse, redirect based on a
2333
+ // `user` that has not loaded yet.
2334
+ // `redirectOrigins` defaults to `[]` — a new array on every render. Used raw
2335
+ // as a dependency, it would re-run the effect in a loop.
2336
+ const redirectOriginsKey = JSON.stringify(redirectOrigins);
2337
+ react.useEffect(() => {
2338
+ if (authLoading || !user) return;
2339
+
2340
+ // `?redirect=` takes priority: whoever arrived through an OAuth flow
2341
+ // needs to go back there, not to the panel's home. Same decision as the
2342
+ // post-sign-in one, with the same origin validation.
2343
+ const target = (handleRedirect ? getRedirectFromLocation(redirectOrigins) : null) || authenticatedRedirect;
2344
+
2345
+ // `authenticatedRedirect={null}` disables the courtesy detour for
2346
+ // someone already signed in — but NOT the return of an OAuth flow, which
2347
+ // is an authorization in progress, not a convenience. The guard above
2348
+ // used to block both together.
2349
+ if (!target) return;
2350
+ applyRedirect(target, navigate);
2351
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- redirectOrigins enters through the serialized key above
2352
+ }, [authLoading, user, authenticatedRedirect, handleRedirect, redirectOriginsKey, navigate]);
2353
+
2354
+ // Step 1 — ask for the code.
2355
+ const handleRequest = async values => {
2356
+ if (sending) return;
2357
+ try {
2358
+ await requestCode(values.email);
2359
+ setSentTo(values.email);
2360
+ setCode('');
2361
+ setCodeError(null);
2362
+ onCodeSent?.(values.email);
2363
+ } catch (error) {
2364
+ onError?.(error);
2365
+ }
2366
+ };
2367
+
2368
+ // Step 2 — trade the code for the session.
2369
+ //
2370
+ // The redirect is decided AND EXECUTED before onSuccess.
2371
+ //
2372
+ // Deciding beforehand was not enough: the app usually calls `navigate('/')`
2373
+ // inside onSuccess, so that navigation happened first and the following
2374
+ // `applyRedirect` ran with the route already changed. `redirectHandled`
2375
+ // protected against that, but only for whoever remembered to honour it — and
2376
+ // half the panels did not, which made the OAuth handoff lose the `#token=`
2377
+ // and end in "Sessão inválida ou expirada".
2378
+ //
2379
+ // Executing here, the OAuth flow's destination no longer depends on each
2380
+ // application getting the contract right. `onSuccess` is still always called
2381
+ // (the app still shows its notification), and `redirectHandled` still
2382
+ // signals that navigation was taken over — now as information, not as a
2383
+ // trap.
2384
+ const handleVerify = async value => {
2385
+ setCodeError(null);
2386
+ try {
2387
+ const result = await verifyCode(sentTo, value);
2388
+ const target = handleRedirect ? getRedirectFromLocation(redirectOrigins) : null;
2389
+ if (target) applyRedirect(target, navigate);
2390
+ onSuccess?.(result?.user ?? null, {
2391
+ result,
2392
+ redirectHandled: !!target
2393
+ });
2394
+ } catch (error) {
2395
+ // The code error belongs to the field, not to the global
2396
+ // notification: the person is looking at the eight characters they
2397
+ // just typed.
2398
+ setCodeError(error?.message || labels.invalidCode || 'Código inválido.');
2399
+ setCode('');
2400
+ onError?.(error);
2401
+ }
2402
+ };
2403
+
2404
+ // Does not render the form for someone who already has a session: the effect
2405
+ // above is redirecting, and showing the fields in that window would flash
2406
+ // the screen "in between" — the symptom the landing linking straight here
2407
+ // exposed. `authLoading` covers the instant before the cookie resolves.
2408
+ //
2409
+ // `?redirect=` counts on its own. In an OAuth flow the panel is a
2410
+ // PASS-THROUGH, not a destination: the token lives in this origin's
2411
+ // localStorage and the worker cannot reach it, so the browser has to come
2412
+ // through here — but the user should not notice. Without this part of the
2413
+ // condition, whoever arrived with an unresolved session still saw the panel
2414
+ // screen before the detour.
2415
+ const oauthPending = handleRedirect && !!getRedirectFromLocation(redirectOrigins);
2416
+ const willRedirect = (!!authenticatedRedirect || oauthPending) && (authLoading || !!user);
2417
+ if (willRedirect) return oauthPending ? redirectingFallback ?? /*#__PURE__*/jsxRuntime.jsx(AuthTransition, {}) : redirectingFallback;
2418
+ return /*#__PURE__*/jsxRuntime.jsx(AuthCard, {
2419
+ logo: finalLogo,
2420
+ logoWidth: logoWidth,
2421
+ title: title,
2422
+ subtitle: sentTo ? labels.codeSent || 'Digite o código que enviamos' : subtitle,
2423
+ variant: variant,
2424
+ opened: opened,
2425
+ onClose: onClose,
2426
+ modalProps: modalProps,
2427
+ ...cardProps,
2428
+ children: !sentTo ? /*#__PURE__*/jsxRuntime.jsx("form", {
2429
+ onSubmit: form$1.onSubmit(handleRequest),
2430
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
2431
+ gap: "md",
2432
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.TextInput, {
2433
+ label: labels.email || 'Email',
2434
+ placeholder: labels.emailPlaceholder || 'seu@email.com',
2435
+ type: "email",
2436
+ autoFocus: true,
2437
+ autoComplete: "email",
2438
+ ...form$1.getInputProps('email'),
2439
+ /*
2440
+ * `readOnly`, not `disabled`: Mantine's `disabled`
2441
+ * fades the field while the request is in flight, and
2442
+ * the value the person just typed is what they are
2443
+ * looking at. `readOnly` locks editing and leaves the
2444
+ * field legible.
2445
+ */
2446
+ readOnly: sending
2447
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
2448
+ type: "submit",
2449
+ fullWidth: true
2450
+ /*
2451
+ * `aria-disabled`, never `disabled`: `disabled`
2452
+ * repaints the button grey, and the white Loader
2453
+ * vanishes inside it — the action IN PROGRESS ends up
2454
+ * weighing less on screen than a button standing
2455
+ * still. With `aria-disabled` the button stays black,
2456
+ * the spinner shows, and the guard on submit blocks
2457
+ * the repeated click.
2458
+ *
2459
+ * And the spinner goes in `leftSection`: Mantine's
2460
+ * `loading` prop hides the label, and a button with
2461
+ * no word does not say what is happening.
2462
+ */,
2463
+ "aria-disabled": sending,
2464
+ leftSection: sending ? /*#__PURE__*/jsxRuntime.jsx(core.Loader, {
2465
+ size: 14,
2466
+ color: "gray.0"
2467
+ }) : null,
2468
+ rightSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconArrowRight, {
2469
+ size: 16
2470
+ }),
2471
+ children: sending ? labels.sendingCode || 'Enviando…' : labels.sendCodeButton || 'Enviar código'
2472
+ }), socialLogin !== false && /*#__PURE__*/jsxRuntime.jsx(SocialButtons, {
2473
+ labels: labels,
2474
+ disabled: sending
2475
+ }), /*#__PURE__*/jsxRuntime.jsx(TermsNotice, {
2476
+ url: termsUrl,
2477
+ text: labels.termsNotice || 'Criando uma conta, você concorda com todos os nossos',
2478
+ linkText: labels.termsLink || 'termos e condições'
2479
+ })]
2480
+ })
2481
+ }) : /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
2482
+ gap: "md",
2483
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.TextInput, {
2484
+ label: labels.codeLabel || 'Código de acesso'
2485
+ /*
2486
+ * The email is the field's description, not the subtitle:
2487
+ * as a subtitle it wrapped onto two lines and competed for
2488
+ * weight with the title. Here it sits where the person
2489
+ * checks it — right above what they are about to type.
2490
+ */,
2491
+ description: `${labels.codeSentTo || 'Enviado para'} ${sentTo}`,
2492
+ placeholder: "ABCD-EFGH"
2493
+ /*
2494
+ * One field, not eight boxes. Eight boxes is the numeric
2495
+ * OTP pattern; this code is alphanumeric and dictated
2496
+ * aloud — in a single field paste works, the screen reader
2497
+ * reads one thing, and the hyphen the person types is
2498
+ * accepted (`normalizeUserCode` in the worker drops it).
2499
+ */,
2500
+ value: code,
2501
+ onChange: event => {
2502
+ setCode(event.currentTarget.value);
2503
+ if (codeError) setCodeError(null);
2504
+ },
2505
+ onKeyDown: event => {
2506
+ if (event.key === 'Enter' && code.trim()) handleVerify(code);
2507
+ },
2508
+ autoFocus: true,
2509
+ autoComplete: "one-time-code",
2510
+ readOnly: verifying,
2511
+ error: codeError
2512
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
2513
+ type: "button",
2514
+ fullWidth: true
2515
+ // Same reason as the previous step: `disabled` would fade
2516
+ // the button exactly while signing in happens. With no
2517
+ // code typed it stays genuinely disabled — there is no
2518
+ // action in progress to hide there.
2519
+ ,
2520
+ "aria-disabled": verifying,
2521
+ disabled: !code.trim(),
2522
+ onClick: verifying ? undefined : () => handleVerify(code),
2523
+ leftSection: verifying ? /*#__PURE__*/jsxRuntime.jsx(core.Loader, {
2524
+ size: 14,
2525
+ color: "gray.0"
2526
+ }) : null,
2527
+ rightSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconArrowRight, {
2528
+ size: 16
2529
+ }),
2530
+ children: verifying ? labels.verifyingCode || 'Entrando…' : labels.confirmCode || 'Confirmar'
2531
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2532
+ justify: "space-between",
2533
+ gap: "xs",
2534
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
2535
+ size: "sm",
2536
+ c: "dimmed",
2537
+ onClick: () => {
2538
+ setSentTo(null);
2539
+ setCode('');
2540
+ setCodeError(null);
2541
+ },
2542
+ children: labels.changeEmail || 'Usar outro e-mail'
2543
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
2544
+ size: "sm",
2545
+ c: "dimmed",
2546
+ onClick: sending ? undefined : () => handleRequest({
2547
+ email: sentTo
2548
+ }),
2549
+ children: sending ? labels.sendingCode || 'Enviando…' : labels.resendCode || 'Reenviar código'
2550
+ })]
2551
+ })]
2552
+ })
2553
+ });
2554
+ }
2555
+
2556
+ function UserProfile({
2557
+ // Variant
2558
+ variant = 'modal',
2559
+ opened,
2560
+ onClose,
2561
+ // Callbacks
2562
+ onProfileUpdate,
2563
+ onSessionRevoked,
2564
+ onOtherSessionsRevoked,
2565
+ onError,
2566
+ // Features toggle
2567
+ showAvatar = true,
2568
+ showName = true,
2569
+ showEmail = true,
2570
+ showSessions = true,
2571
+ // Customização
2572
+ labels = {},
2573
+ title = 'Account',
2574
+ subtitle = 'Manage your account info.',
2575
+ logo,
2576
+ logoHeight = 28,
2577
+ width = 500,
2578
+ // Avatar config
2579
+ maxAvatarSize = 500 * 1024,
2580
+ // 500KB
2581
+
2582
+ // Custom sections (React nodes to render after built-in sections)
2583
+ customSections,
2584
+ ...containerProps
2585
+ }) {
2586
+ // Local state - which section is expanded
2587
+ const [editingSection, setEditingSection] = react.useState(null); // 'password' | 'email' | 'name' | 'avatar' | null
2588
+
2589
+ // Hook para profile
2590
+ const {
2591
+ user,
2592
+ updateProfile,
2593
+ loadingUpdateProfile
2594
+ } = useUser();
2595
+
2596
+ // Hook para sessions
2597
+ const {
2598
+ currentSession,
2599
+ sessions,
2600
+ listSessions,
2601
+ getSession,
2602
+ revokeSession,
2603
+ revokeOtherSessions,
2604
+ loadingListSessions,
2605
+ loadingRevokeSession
2606
+ } = useSessions();
2607
+
2608
+ // Load sessions when opened (modal) or component mounts (card), and when sessions section is opened
2609
+ react.useEffect(() => {
2610
+ if (showSessions && (variant === 'card' || opened)) {
2611
+ // Fetch current session first to get the session ID, then list all sessions
2612
+ getSession().catch(err => console.warn('Failed to get current session:', err));
2613
+ listSessions().catch(err => console.warn('Failed to load sessions:', err));
2614
+ }
2615
+ }, [opened, showSessions, variant]);
2616
+
2617
+ // Helper to parse user agent string
2618
+ const parseUserAgent = ua => {
2619
+ if (!ua) return {
2620
+ browser: 'Unknown Browser',
2621
+ os: 'Unknown OS'
2622
+ };
2623
+ let browser = 'Unknown Browser';
2624
+ let os = 'Unknown OS';
2625
+
2626
+ // Detect browser
2627
+ if (ua.includes('Chrome') && !ua.includes('Edg')) browser = 'Chrome';else if (ua.includes('Firefox')) browser = 'Firefox';else if (ua.includes('Safari') && !ua.includes('Chrome')) browser = 'Safari';else if (ua.includes('Edg')) browser = 'Edge';else if (ua.includes('Opera') || ua.includes('OPR')) browser = 'Opera';
2628
+
2629
+ // Detect OS
2630
+ if (ua.includes('Windows')) os = 'Windows';else if (ua.includes('Mac OS')) os = 'macOS';else if (ua.includes('Linux')) os = 'Linux';else if (ua.includes('Android')) os = 'Android';else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS';
2631
+ return {
2632
+ browser,
2633
+ os
2634
+ };
2635
+ };
2636
+
2637
+ // Session handlers
2638
+ const handleRevokeSession = async sessionId => {
2639
+ // Check if revoking current session
2640
+ const isCurrentSession = sessionId === currentSession?.id || sessions.length === 1;
2641
+
2642
+ // If revoking current session, handle logout immediately after revocation
2643
+ if (isCurrentSession) {
2644
+ try {
2645
+ await revokeSession(sessionId);
2646
+ onSessionRevoked?.(sessionId);
2647
+ } catch (error) {
2648
+ onError?.(error);
2649
+ // Even if it fails, we're revoking our own session, so just logout
2650
+ // The server already revoked our session
2651
+ }
2652
+ // Clear auth state and redirect
2653
+ localStorage.removeItem('auth:token');
2654
+ window.dispatchEvent(new CustomEvent('auth:session-revoked'));
2655
+ if (variant === 'modal') onClose?.();
2656
+ return;
2657
+ }
2658
+
2659
+ // Revoking another session
2660
+ try {
2661
+ await revokeSession(sessionId);
2662
+ onSessionRevoked?.(sessionId);
2663
+ } catch (error) {
2664
+ // Check for 401 error (our SDK uses error.res, axios uses error.response)
2665
+ const status = error.res?.status || error.response?.status;
2666
+ if (status === 401) {
2667
+ // This means our session was revoked, not the target one - do logout
2668
+ localStorage.removeItem('auth:token');
2669
+ window.dispatchEvent(new CustomEvent('auth:session-revoked'));
2670
+ if (variant === 'modal') onClose?.();
2671
+ return;
2672
+ }
2673
+ onError?.(error);
2674
+ }
2675
+ };
2676
+ const handleRevokeOtherSessions = async () => {
2677
+ try {
2678
+ await revokeOtherSessions();
2679
+ onOtherSessionsRevoked?.();
2680
+ } catch (error) {
2681
+ onError?.(error);
2682
+ }
2683
+ };
2684
+
2685
+ // Name form
2686
+ const nameForm = form.useForm({
2687
+ initialValues: {
2688
+ name: ''
2689
+ },
2690
+ validate: {
2691
+ name: v => !v ? labels.nameRequired || 'Nome obrigatório' : null
2692
+ }
2693
+ });
2694
+
2695
+ // Avatar state (base64)
2696
+ const [avatarPreview, setAvatarPreview] = react.useState(null);
2697
+ const [avatarFile, setAvatarFile] = react.useState(null);
2698
+
2699
+ // Handle file selection and convert to base64
2700
+ const handleAvatarFileChange = file => {
2701
+ if (!file) {
2702
+ setAvatarPreview(null);
2703
+ setAvatarFile(null);
2704
+ return;
2705
+ }
2706
+
2707
+ // Validate file type
2708
+ if (!file.type.startsWith('image/')) {
2709
+ onError?.(new Error(labels.avatarInvalidType || 'Por favor, selecione uma imagem válida'));
2710
+ return;
2711
+ }
2712
+
2713
+ // Validate file size
2714
+ if (file.size > maxAvatarSize) {
2715
+ onError?.(new Error(labels.avatarTooLarge || `Imagem muito grande. Máximo ${Math.round(maxAvatarSize / 1024)}KB.`));
2716
+ return;
2717
+ }
2718
+ setAvatarFile(file);
2719
+
2720
+ // Convert to base64
2721
+ const reader = new FileReader();
2722
+ reader.onloadend = () => {
2723
+ setAvatarPreview(reader.result);
2724
+ };
2725
+ reader.readAsDataURL(file);
2726
+ };
2727
+
2728
+ // Populate forms when user data is available or section opens
2729
+ react.useEffect(() => {
2730
+ if (editingSection === 'name' && user?.name) {
2731
+ nameForm.setValues({
2732
+ name: user.name
2733
+ });
2734
+ }
2735
+ if (editingSection === 'avatar' && user?.image) {
2736
+ setAvatarPreview(user.image);
2737
+ }
2738
+ }, [editingSection, user]);
2739
+ const handleToggleSection = section => {
2740
+ if (editingSection === section) {
2741
+ setEditingSection(null);
2742
+ nameForm.reset();
2743
+ setAvatarPreview(null);
2744
+ setAvatarFile(null);
2745
+ } else {
2746
+ setEditingSection(section);
2747
+ }
2748
+ };
2749
+ const handleChangeName = async values => {
2750
+ try {
2751
+ await updateProfile({
2752
+ name: values.name
2753
+ });
2754
+ nameForm.reset();
2755
+ setEditingSection(null);
2756
+ onProfileUpdate?.({
2757
+ name: values.name
2758
+ });
2759
+ } catch (error) {
2760
+ onError?.(error);
2761
+ }
2762
+ };
2763
+ const handleChangeAvatar = async () => {
2764
+ if (!avatarPreview) {
2765
+ // Optionally handle this validation error via callback or just return?
2766
+ // Since it's a validation error before async call, we might want to expose it too?
2767
+ // The original code used notifications. Let's send to onError for consistency or just return if it's UI state.
2768
+ // Actually, for validation within the component, maybe we can just let it be silent or use form error if applicable?
2769
+ // But this is outside form context. Let's use onError with a custom error object.
2770
+ onError?.(new Error(labels.avatarRequired || 'Selecione uma imagem'));
2771
+ return;
2772
+ }
2773
+ try {
2774
+ await updateProfile({
2775
+ image: avatarPreview
2776
+ });
2777
+ setAvatarPreview(null);
2778
+ setAvatarFile(null);
2779
+ setEditingSection(null);
2780
+ onProfileUpdate?.({
2781
+ image: avatarPreview
2782
+ });
2783
+ } catch (error) {
2784
+ onError?.(error);
2785
+ }
2786
+ };
2787
+ const handleRemoveAvatar = async () => {
2788
+ try {
2789
+ await updateProfile({
2790
+ image: ''
2791
+ });
2792
+ setAvatarPreview(null);
2793
+ setAvatarFile(null);
2794
+ setEditingSection(null);
2795
+ onProfileUpdate?.({
2796
+ image: ''
2797
+ });
2798
+ } catch (error) {
2799
+ onError?.(error);
2800
+ }
2801
+ };
2802
+
2803
+ // Reusable Section Header
2804
+ const SectionHeader = ({
2805
+ icon: Icon,
2806
+ sectionTitle,
2807
+ description
2808
+ }) => /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2809
+ gap: "sm",
2810
+ mb: "lg",
2811
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.ThemeIcon, {
2812
+ size: 36,
2813
+ variant: "subtle",
2814
+ color: "gray",
2815
+ children: /*#__PURE__*/jsxRuntime.jsx(Icon, {
2816
+ size: 28,
2817
+ stroke: 1.5
2818
+ })
2819
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
2820
+ gap: 0,
2821
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
2822
+ fw: 600,
2823
+ size: "sm",
2824
+ children: sectionTitle
2825
+ }), description && /*#__PURE__*/jsxRuntime.jsx(core.Text, {
2826
+ size: "xs",
2827
+ c: "dimmed",
2828
+ children: description
2829
+ })]
2830
+ })]
2831
+ });
2832
+
2833
+ // Reusable Row component
2834
+ const SettingRow = ({
2835
+ label,
2836
+ children,
2837
+ action,
2838
+ actionLabel,
2839
+ onClick,
2840
+ expanded
2841
+ }) => /*#__PURE__*/jsxRuntime.jsx(core.Box, {
2842
+ py: "xs",
2843
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2844
+ justify: "space-between",
2845
+ wrap: "nowrap",
2846
+ align: "center",
2847
+ children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2848
+ gap: "xl",
2849
+ wrap: "nowrap",
2850
+ flex: 1,
2851
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
2852
+ size: "sm",
2853
+ c: "dimmed",
2854
+ w: 100,
2855
+ children: label
2856
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Box, {
2857
+ flex: 1,
2858
+ children: children
2859
+ })]
2860
+ }), action && /*#__PURE__*/jsxRuntime.jsx(core.Tooltip, {
2861
+ label: actionLabel || action,
2862
+ position: "left",
2863
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
2864
+ size: "xs",
2865
+ fw: 300,
2866
+ onClick: onClick,
2867
+ c: "gray",
2868
+ underline: "none",
2869
+ children: expanded ? labels.cancel || 'Cancel' : action
2870
+ })
2871
+ })]
2872
+ })
2873
+ });
2874
+
2875
+ // Conteúdo interno compartilhado
2876
+ const profileContent = /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
2877
+ children: [(showAvatar || showName || showEmail) && /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
2878
+ mb: "lg",
2879
+ children: [/*#__PURE__*/jsxRuntime.jsx(SectionHeader, {
2880
+ icon: iconsReact.IconUser,
2881
+ sectionTitle: labels.profileSection || 'Profile',
2882
+ description: labels.profileDescription || 'Your personal information'
2883
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
2884
+ gap: "sm",
2885
+ children: [showAvatar && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
2886
+ children: [/*#__PURE__*/jsxRuntime.jsx(SettingRow, {
2887
+ label: labels.avatar || 'Avatar',
2888
+ action: labels.update || 'Update',
2889
+ actionLabel: labels.updateAvatar || 'Update your profile picture',
2890
+ onClick: () => handleToggleSection('avatar'),
2891
+ expanded: editingSection === 'avatar',
2892
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Group, {
2893
+ gap: "md",
2894
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Avatar, {
2895
+ src: user?.image,
2896
+ name: user?.name || user?.email,
2897
+ size: 48,
2898
+ radius: "xl"
2899
+ // color="initials"
2900
+ })
2901
+ })
2902
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Collapse, {
2903
+ in: editingSection === 'avatar',
2904
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
2905
+ p: "sm",
2906
+ withBorder: true,
2907
+ radius: "sm",
2908
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
2909
+ gap: "md",
2910
+ align: "center",
2911
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.FileButton, {
2912
+ onChange: handleAvatarFileChange,
2913
+ accept: "image/*",
2914
+ children: props => /*#__PURE__*/jsxRuntime.jsx(core.Tooltip, {
2915
+ label: labels.clickToChange || 'Clique para alterar',
2916
+ position: "bottom",
2917
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
2918
+ ...props,
2919
+ pos: "relative",
2920
+ style: {
2921
+ cursor: 'pointer'
2922
+ },
2923
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Avatar, {
2924
+ src: avatarPreview || user?.image,
2925
+ name: user?.name || user?.email,
2926
+ size: 80,
2927
+ radius: 80,
2928
+ color: "gray"
2929
+ }), /*#__PURE__*/jsxRuntime.jsx(core.ThemeIcon, {
2930
+ size: 26,
2931
+ radius: "xl",
2932
+ color: "gray",
2933
+ pos: "absolute",
2934
+ bottom: 0,
2935
+ right: 0,
2936
+ bd: "2px solid body",
2937
+ children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconPhoto, {
2938
+ size: 14,
2939
+ stroke: 1.5
2940
+ })
2941
+ })]
2942
+ })
2943
+ })
2944
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
2945
+ size: "xs",
2946
+ c: "dimmed",
2947
+ ta: "center",
2948
+ children: labels.avatarHint || `Máximo ${Math.round(maxAvatarSize / 1024)}KB • JPG, PNG, GIF, WebP`
2949
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2950
+ justify: "center",
2951
+ gap: "xs",
2952
+ children: [user?.image && /*#__PURE__*/jsxRuntime.jsx(core.Button, {
2953
+ variant: "subtle",
2954
+ color: "gray",
2955
+ size: "xs",
2956
+ onClick: handleRemoveAvatar,
2957
+ loading: loadingUpdateProfile,
2958
+ loaderProps: {
2959
+ size: 12
2960
+ },
2961
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconTrash, {
2962
+ size: 14,
2963
+ stroke: 1.5
2964
+ }),
2965
+ children: labels.remove || 'Remover'
2966
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
2967
+ variant: "default",
2968
+ size: "xs",
2969
+ onClick: () => handleToggleSection('avatar'),
2970
+ children: labels.cancel || 'Cancelar'
2971
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
2972
+ size: "xs",
2973
+ loading: loadingUpdateProfile,
2974
+ loaderProps: {
2975
+ size: 12
2976
+ },
2977
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconCheck, {
2978
+ size: 14,
2979
+ stroke: 1.5
2980
+ }),
2981
+ onClick: handleChangeAvatar,
2982
+ disabled: !avatarPreview || avatarPreview === user?.image,
2983
+ children: labels.save || 'Salvar'
2984
+ })]
2985
+ })]
2986
+ })
2987
+ })
2988
+ })]
2989
+ }), showName && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
2990
+ children: [/*#__PURE__*/jsxRuntime.jsx(SettingRow, {
2991
+ label: labels.name || 'Nome',
2992
+ action: labels.change || 'Change',
2993
+ actionLabel: labels.changeName || 'Change your display name',
2994
+ onClick: () => handleToggleSection('name'),
2995
+ expanded: editingSection === 'name',
2996
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
2997
+ gap: "xs",
2998
+ children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconPencil, {
2999
+ size: 18,
3000
+ stroke: 1.5,
3001
+ color: "var(--mantine-color-dimmed)"
3002
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3003
+ size: "sm",
3004
+ children: user?.name || labels.notDefined || 'Não definido'
3005
+ })]
3006
+ })
3007
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Collapse, {
3008
+ in: editingSection === 'name',
3009
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
3010
+ p: "sm",
3011
+ withBorder: true,
3012
+ radius: "sm",
3013
+ children: /*#__PURE__*/jsxRuntime.jsx("form", {
3014
+ onSubmit: nameForm.onSubmit(handleChangeName),
3015
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3016
+ gap: "sm",
3017
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.TextInput, {
3018
+ label: labels.name || 'Nome',
3019
+ placeholder: labels.namePlaceholder || 'Digite seu nome',
3020
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconUser, {
3021
+ size: 16,
3022
+ stroke: 1.5
3023
+ }),
3024
+ ...nameForm.getInputProps('name')
3025
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3026
+ justify: "flex-end",
3027
+ gap: "xs",
3028
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Button, {
3029
+ variant: "default",
3030
+ size: "xs",
3031
+ onClick: () => handleToggleSection('name'),
3032
+ children: labels.cancel || 'Cancelar'
3033
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
3034
+ type: "submit",
3035
+ size: "xs",
3036
+ loading: loadingUpdateProfile,
3037
+ loaderProps: {
3038
+ size: 12
3039
+ },
3040
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconCheck, {
3041
+ size: 14,
3042
+ stroke: 1.5
3043
+ }),
3044
+ children: labels.save || 'Salvar'
3045
+ })]
3046
+ })]
3047
+ })
3048
+ })
3049
+ })
3050
+ })]
3051
+ }), showEmail && /*#__PURE__*/jsxRuntime.jsx(SettingRow, {
3052
+ label: labels.email || 'Email',
3053
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3054
+ gap: "xs",
3055
+ children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconMail, {
3056
+ size: 18,
3057
+ stroke: 1.5,
3058
+ color: "var(--mantine-color-dimmed)"
3059
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3060
+ size: "sm",
3061
+ children: user?.email || 'email@exemplo.com'
3062
+ })]
3063
+ })
3064
+ })]
3065
+ })]
3066
+ }), showSessions && /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
3067
+ mb: "md",
3068
+ children: [/*#__PURE__*/jsxRuntime.jsx(SectionHeader, {
3069
+ icon: iconsReact.IconShield,
3070
+ sectionTitle: labels.securitySection || 'Security',
3071
+ description: labels.securityDescription || 'Protect your account'
3072
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Stack, {
3073
+ gap: "sm",
3074
+ children: showSessions && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
3075
+ children: [/*#__PURE__*/jsxRuntime.jsx(SettingRow, {
3076
+ label: labels.sessions || 'Sessions',
3077
+ action: editingSection === 'sessions' ? labels.close || 'Close' : labels.manage || 'Manage',
3078
+ actionLabel: labels.manageSessions || 'Manage your active sessions',
3079
+ onClick: () => handleToggleSection('sessions'),
3080
+ expanded: editingSection === 'sessions',
3081
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3082
+ gap: "xs",
3083
+ children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconDevices, {
3084
+ size: 18,
3085
+ stroke: 1.5,
3086
+ color: "var(--mantine-color-dimmed)"
3087
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3088
+ size: "sm",
3089
+ c: "dimmed",
3090
+ children: sessions.length > 0 ? `${sessions.length} active session${sessions.length > 1 ? 's' : ''}` : loadingListSessions ? 'Loading...' : 'No sessions'
3091
+ })]
3092
+ })
3093
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Collapse, {
3094
+ in: editingSection === 'sessions',
3095
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
3096
+ p: "sm",
3097
+ withBorder: true,
3098
+ radius: "sm",
3099
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Stack, {
3100
+ gap: "xs",
3101
+ children: loadingListSessions ? /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3102
+ size: "xs",
3103
+ c: "dimmed",
3104
+ ta: "center",
3105
+ py: "md",
3106
+ children: labels.loadingSessions || 'Carregando sessões...'
3107
+ }) : sessions.length === 0 ? /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3108
+ size: "xs",
3109
+ c: "dimmed",
3110
+ ta: "center",
3111
+ py: "md",
3112
+ children: labels.noSessionsFound || 'Nenhuma sessão encontrada'
3113
+ }) : /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
3114
+ children: [sessions.map(sessionItem => {
3115
+ const isCurrentSession = sessionItem.id === currentSession?.id;
3116
+ const deviceInfo = parseUserAgent(sessionItem.userAgent);
3117
+ const createdDate = new Date(sessionItem.createdAt);
3118
+ return /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
3119
+ p: "xs",
3120
+ withBorder: isCurrentSession,
3121
+ bd: isCurrentSession ? '1px solid gray' : undefined,
3122
+ radius: "sm",
3123
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3124
+ justify: "space-between",
3125
+ wrap: "nowrap",
3126
+ align: "center",
3127
+ children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3128
+ gap: "sm",
3129
+ wrap: "nowrap",
3130
+ flex: 1,
3131
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.ThemeIcon, {
3132
+ size: 32,
3133
+ variant: "subtle",
3134
+ color: "gray",
3135
+ children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconDeviceMobile, {
3136
+ size: 18,
3137
+ stroke: 1.5
3138
+ })
3139
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
3140
+ flex: 1,
3141
+ children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3142
+ gap: "xs",
3143
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
3144
+ size: "xs",
3145
+ fw: 600,
3146
+ children: deviceInfo.browser
3147
+ }), isCurrentSession && /*#__PURE__*/jsxRuntime.jsx(core.Badge, {
3148
+ size: "xs",
3149
+ variant: "light",
3150
+ color: "gray",
3151
+ children: labels.thisDevice || 'Este dispositivo'
3152
+ })]
3153
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Text, {
3154
+ size: "xs",
3155
+ c: "dimmed",
3156
+ children: [deviceInfo.os, " \u2022 ", sessionItem.ipAddress || labels.unknownIP || 'IP desconhecido']
3157
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Text, {
3158
+ size: "xs",
3159
+ c: "dimmed",
3160
+ children: [labels.createdAt || 'Criada em', " ", createdDate.toLocaleDateString('pt-BR'), " ", labels.at || 'às', ' ', createdDate.toLocaleTimeString('pt-BR', {
3161
+ hour: '2-digit',
3162
+ minute: '2-digit'
3163
+ })]
3164
+ })]
3165
+ })]
3166
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Tooltip, {
3167
+ label: isCurrentSession ? labels.signOutAndEnd || 'Encerrar e sair' : labels.endSession || 'Encerrar sessão',
3168
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Button, {
3169
+ variant: "subtle",
3170
+ color: "gray",
3171
+ size: "xs",
3172
+ onClick: () => handleRevokeSession(sessionItem.id),
3173
+ loading: loadingRevokeSession === sessionItem.id,
3174
+ loaderProps: {
3175
+ size: 12
3176
+ },
3177
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconLogout, {
3178
+ size: 14,
3179
+ stroke: 1.5
3180
+ }),
3181
+ children: labels.end || 'Encerrar'
3182
+ })
3183
+ })]
3184
+ })
3185
+ }, sessionItem.id);
3186
+ }), sessions.length > 1 && /*#__PURE__*/jsxRuntime.jsx(core.Group, {
3187
+ justify: "flex-end",
3188
+ mt: "xs",
3189
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Button, {
3190
+ variant: "subtle",
3191
+ color: "gray",
3192
+ size: "xs",
3193
+ onClick: handleRevokeOtherSessions,
3194
+ loading: loadingRevokeSession === 'all',
3195
+ loaderProps: {
3196
+ size: 12
3197
+ },
3198
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconLogout, {
3199
+ size: 14,
3200
+ stroke: 1.5
3201
+ }),
3202
+ children: labels.endOtherSessions || 'Encerrar todas as outras sessões'
3203
+ })
3204
+ })]
3205
+ })
3206
+ })
3207
+ })
3208
+ })]
3209
+ })
3210
+ })]
3211
+ }), customSections]
3212
+ });
3213
+
3214
+ // Renderizar como Modal
3215
+ if (variant === 'modal') {
3216
+ return /*#__PURE__*/jsxRuntime.jsxs(core.Modal, {
3217
+ opened: opened,
3218
+ onClose: onClose,
3219
+ size: width,
3220
+ withCloseButton: true,
3221
+ radius: "md",
3222
+ overlayProps: {
3223
+ backgroundOpacity: 0.5,
3224
+ blur: 4
3225
+ },
3226
+ title: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3227
+ gap: "sm",
3228
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.ThemeIcon, {
3229
+ size: 32,
3230
+ variant: "subtle",
3231
+ color: "gray",
3232
+ children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconUserCircle, {
3233
+ size: 24,
3234
+ stroke: 1.5
3235
+ })
3236
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3237
+ gap: 0,
3238
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Title, {
3239
+ order: 5,
3240
+ fw: 600,
3241
+ children: title
3242
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3243
+ size: "xs",
3244
+ c: "dimmed",
3245
+ children: subtitle
3246
+ })]
3247
+ })]
3248
+ }),
3249
+ ...containerProps,
3250
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Divider, {
3251
+ mb: "md"
3252
+ }), profileContent]
3253
+ });
3254
+ }
3255
+
3256
+ // Renderizar como Card
3257
+ return /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
3258
+ withBorder: true
3259
+ // shadow="md"
3260
+ ,
3261
+ p: "md",
3262
+ w: width,
3263
+ radius: "md",
3264
+ ...containerProps,
3265
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3266
+ gap: "sm",
3267
+ children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3268
+ gap: "sm",
3269
+ children: [logo ? /*#__PURE__*/jsxRuntime.jsx(core.Image, {
3270
+ src: logo,
3271
+ alt: "Auth",
3272
+ h: logoHeight,
3273
+ fit: "contain"
3274
+ }) : /*#__PURE__*/jsxRuntime.jsx(core.ThemeIcon, {
3275
+ size: 32,
3276
+ variant: "subtle",
3277
+ color: "gray",
3278
+ children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconUserCircle, {
3279
+ size: 24,
3280
+ stroke: 1.5
3281
+ })
3282
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3283
+ gap: 0,
3284
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Title, {
3285
+ order: 5,
3286
+ fw: 600,
3287
+ children: title
3288
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3289
+ size: "xs",
3290
+ c: "dimmed",
3291
+ children: subtitle
3292
+ })]
3293
+ })]
3294
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Divider, {}), profileContent]
3295
+ })
3296
+ });
3297
+ }
3298
+
3299
+ function UserInformation({
3300
+ user,
3301
+ signOut,
3302
+ onAccountClick,
3303
+ onBillingClick,
3304
+ accountLabel = 'Conta',
3305
+ billingLabel = 'Assinatura',
3306
+ padded = true,
3307
+ size = 'sm',
3308
+ style,
3309
+ ...others
3310
+ }) {
3311
+ if (!user) return null;
3312
+
3313
+ // Size mappings
3314
+ const avatarSizeMap = {
3315
+ sm: 32,
3316
+ md: 40,
3317
+ lg: 48
3318
+ };
3319
+ const fontSizeTitleMap = {
3320
+ sm: 'sm',
3321
+ md: 'md',
3322
+ lg: 'lg'
3323
+ };
3324
+ const fontSizeEmailMap = {
3325
+ sm: '10px',
3326
+ md: 'xs',
3327
+ lg: 'sm'
3328
+ };
3329
+ const btnSizeMap = {
3330
+ sm: 'xs',
3331
+ md: 'sm',
3332
+ lg: 'md'
3333
+ };
3334
+ const gapMap = {
3335
+ sm: 'xs',
3336
+ md: 'sm',
3337
+ lg: 'md'
3338
+ };
3339
+ const widthMap = {
3340
+ sm: 280,
3341
+ md: 320,
3342
+ lg: 400
3343
+ };
3344
+ const name = user.fullName || user.name || 'User';
3345
+ const email = user.primaryEmailAddress || user.email || '';
3346
+ const initials = name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
3347
+ return /*#__PURE__*/jsxRuntime.jsx(core.Box, {
3348
+ p: padded ? gapMap[size] : 0,
3349
+ w: widthMap[size],
3350
+ style: style,
3351
+ ...others,
3352
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3353
+ gap: gapMap[size],
3354
+ children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3355
+ wrap: "nowrap",
3356
+ gap: "xs",
3357
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Avatar, {
3358
+ src: user.imageUrl || user.image,
3359
+ size: avatarSizeMap[size],
3360
+ radius: "xl",
3361
+ bg: "gray.1",
3362
+ c: "gray.6",
3363
+ styles: {
3364
+ placeholder: {
3365
+ fontSize: core.rem(avatarSizeMap[size] / 2.2),
3366
+ fontWeight: 600
3367
+ }
3368
+ },
3369
+ children: initials || 'CC'
3370
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
3371
+ style: {
3372
+ flex: 1,
3373
+ overflow: 'hidden'
3374
+ },
3375
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
3376
+ size: fontSizeTitleMap[size],
3377
+ fw: 700,
3378
+ truncate: "end",
3379
+ c: "dark.9",
3380
+ lh: 1.1,
3381
+ children: name
3382
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3383
+ size: fontSizeEmailMap[size],
3384
+ c: "gray.5",
3385
+ truncate: "end",
3386
+ lh: 1.1,
3387
+ children: email
3388
+ })]
3389
+ }), /*#__PURE__*/jsxRuntime.jsx(core.ActionIcon, {
3390
+ size: btnSizeMap[size],
3391
+ onClick: signOut,
3392
+ children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconLogout, {
3393
+ size: 16,
3394
+ stroke: 1.5
3395
+ })
3396
+ })]
3397
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3398
+ grow: true,
3399
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Button, {
3400
+ variant: "default",
3401
+ size: btnSizeMap[size],
3402
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconSettings, {
3403
+ size: 16,
3404
+ stroke: 1.5
3405
+ }),
3406
+ onClick: onAccountClick,
3407
+ children: accountLabel
3408
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
3409
+ variant: "default",
3410
+ size: btnSizeMap[size],
3411
+ leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconCreditCard, {
3412
+ size: 16,
3413
+ stroke: 1.5
3414
+ }),
3415
+ onClick: onBillingClick,
3416
+ children: billingLabel
3417
+ })]
3418
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3419
+ justify: "center",
3420
+ gap: 4,
3421
+ opacity: 0.3,
3422
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
3423
+ size: "10px",
3424
+ c: "gray.6",
3425
+ fw: 600,
3426
+ children: "Secured by"
3427
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3428
+ gap: 2,
3429
+ children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconShieldCheck, {
3430
+ size: 10,
3431
+ stroke: 2
3432
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3433
+ size: "10px",
3434
+ fw: 800,
3435
+ c: "dark.9",
3436
+ children: "Auth"
3437
+ })]
3438
+ })]
3439
+ })]
3440
+ })
3441
+ });
3442
+ }
3443
+
3444
+ /**
3445
+ * Renderiza children apenas quando o usuário está autenticado
3446
+ * Equivalente ao <SignedIn> do Clerk
3447
+ */
3448
+ function SignedIn({
3449
+ children
3450
+ }) {
3451
+ const {
3452
+ user,
3453
+ loading
3454
+ } = useAuth();
3455
+ if (loading || !user) return null;
3456
+ return children;
3457
+ }
3458
+
3459
+ /**
3460
+ * Renderiza children apenas quando o usuário NÃO está autenticado
3461
+ * Equivalente ao <SignedOut> do Clerk
3462
+ */
3463
+ function SignedOut({
3464
+ children
3465
+ }) {
3466
+ const {
3467
+ user,
3468
+ loading
3469
+ } = useAuth();
3470
+ if (loading || user) return null;
3471
+ return children;
3472
+ }
3473
+
3474
+ /**
3475
+ * Renderiza children enquanto a autenticação está carregando
3476
+ * Equivalente ao <ClerkLoading> do Clerk
3477
+ */
3478
+ function AuthLoading({
3479
+ children
3480
+ }) {
3481
+ const {
3482
+ loading
3483
+ } = useAuth();
3484
+ if (!loading) return null;
3485
+ return children;
3486
+ }
3487
+
3488
+ /**
3489
+ * Renderiza children quando a autenticação terminou de carregar
3490
+ * Equivalente ao <ClerkLoaded> do Clerk
3491
+ */
3492
+ function AuthLoaded({
3493
+ children
3494
+ }) {
3495
+ const {
3496
+ loading
3497
+ } = useAuth();
3498
+ if (loading) return null;
3499
+ return children;
3500
+ }
3501
+
3502
+ function SignInButton({
3503
+ children,
3504
+ redirectTo = '/login',
3505
+ ...props
3506
+ }) {
3507
+ const navigate = reactRouterDom.useNavigate();
3508
+ return /*#__PURE__*/jsxRuntime.jsx("button", {
3509
+ onClick: () => navigate(redirectTo),
3510
+ ...props,
3511
+ children: children || 'Sign In'
3512
+ });
3513
+ }
3514
+
3515
+ function SignOutButton({
3516
+ children,
3517
+ onSignOut,
3518
+ ...props
3519
+ }) {
3520
+ const signOut = useSignOut();
3521
+ const handleClick = async () => {
3522
+ await signOut();
3523
+ onSignOut?.();
3524
+ };
3525
+ return /*#__PURE__*/jsxRuntime.jsx("button", {
3526
+ onClick: handleClick,
3527
+ ...props,
3528
+ children: children || 'Sign Out'
3529
+ });
3530
+ }
3531
+
3532
+ exports.AuthCard = AuthCard;
3533
+ exports.AuthLoaded = AuthLoaded;
3534
+ exports.AuthLoading = AuthLoading;
3535
+ exports.AuthProvider = AuthProvider;
3536
+ exports.GuestOnly = GuestOnly;
3537
+ exports.IDENTITY_CHANGED_EVENT = IDENTITY_CHANGED_EVENT;
3538
+ exports.Protect = Protect;
3539
+ exports.SignIn = SignIn;
3540
+ exports.SignInButton = SignInButton;
3541
+ exports.SignOutButton = SignOutButton;
3542
+ exports.SignedIn = SignedIn;
3543
+ exports.SignedOut = SignedOut;
3544
+ exports.SocialButtons = SocialButtons;
3545
+ exports.TOKEN_STORAGE_KEY = TOKEN_STORAGE_KEY;
3546
+ exports.UserInformation = UserInformation;
3547
+ exports.UserProfile = UserProfile;
3548
+ exports.Wordmark = Wordmark;
3549
+ exports.announceIdentityChange = announceIdentityChange;
3550
+ exports.applyRedirect = applyRedirect;
3551
+ exports.clearIdentitySwitching = clearIdentitySwitching;
3552
+ exports.configure = configure;
3553
+ exports.consumeSocialError = consumeSocialError;
3554
+ exports.consumeSocialToken = consumeSocialToken;
3555
+ exports.decodeJWT = decodeJWT;
3556
+ exports.endImpersonation = endImpersonation;
3557
+ exports.getApiUrl = getApiUrl;
3558
+ exports.getApplicationInfo = getApplicationInfo;
3559
+ exports.getCurrentUser = getCurrentUser;
3560
+ exports.getLinkedProviders = getLinkedProviders;
3561
+ exports.getRedirectFromLocation = getRedirectFromLocation;
3562
+ exports.getSession = getSession;
3563
+ exports.getSocialProviders = getSocialProviders;
3564
+ exports.isAuthenticated = isAuthenticated;
3565
+ exports.isIdentitySwitching = isIdentitySwitching;
3566
+ exports.isInternal = isInternal;
3567
+ exports.listSessions = listSessions;
3568
+ exports.markIdentitySwitching = markIdentitySwitching;
3569
+ exports.pollCode = pollCode;
3570
+ exports.refreshToken = refreshToken;
3571
+ exports.requestCode = requestCode;
3572
+ exports.resolveRedirect = resolveRedirect;
3573
+ exports.revokeOtherSessions = revokeOtherSessions;
3574
+ exports.revokeSession = revokeSession;
3575
+ exports.setStoredToken = setStoredToken;
3576
+ exports.shouldSignOutOn401 = shouldSignOutOn401;
3577
+ exports.signOut = signOut;
3578
+ exports.startSocialLink = startSocialLink;
3579
+ exports.startSocialSignIn = startSocialSignIn;
3580
+ exports.unlinkSocialProvider = unlinkSocialProvider;
3581
+ exports.updateProfile = updateProfile;
3582
+ exports.useApplicationLogo = useApplicationLogo;
3583
+ exports.useAuth = useAuth;
3584
+ exports.useAuthLoading = useAuthLoading;
3585
+ exports.useAuthStore = useAuthStore;
3586
+ exports.useCheckToken = useCheckToken;
3587
+ exports.useImpersonation = useImpersonation;
3588
+ exports.useSession = useSession;
3589
+ exports.useSessions = useSessions;
3590
+ exports.useSignIn = useSignIn;
3591
+ exports.useSignOut = useSignOut;
3592
+ exports.useUser = useUser;
3593
+ exports.verifyCode = verifyCode;
3594
+ //# sourceMappingURL=index.js.map