@multiplatform.one/keycloak 6.7.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/cjs/index.cjs +129 -55
  2. package/dist/cjs/index.native.cjs +239 -130
  3. package/dist/esm/index.js +131 -57
  4. package/dist/esm/index.native.js +239 -130
  5. package/package.json +15 -10
  6. package/src/betterAuth/expoAuthFlow.native.spec.ts +307 -0
  7. package/src/betterAuth/expoAuthFlow.native.ts +189 -55
  8. package/src/frappeToken.native.spec.ts +104 -0
  9. package/src/frappeToken.native.ts +46 -2
  10. package/src/frappeToken.spec.ts +141 -0
  11. package/src/frappeToken.ts +106 -14
  12. package/src/index.ts +1 -1
  13. package/src/keycloak/index.ts +24 -7
  14. package/src/keycloak/index.webext.ts +16 -0
  15. package/src/oauth/authorizationUrl.spec.ts +83 -0
  16. package/src/oauth/authorizationUrl.ts +74 -0
  17. package/src/oauth/callback.spec.ts +72 -0
  18. package/src/oauth/callback.ts +46 -0
  19. package/src/oauth/callbackPage.spec.ts +61 -0
  20. package/src/oauth/callbackPage.ts +57 -0
  21. package/src/oauth/endpoints.spec.ts +193 -0
  22. package/src/oauth/endpoints.ts +135 -0
  23. package/src/oauth/errors.ts +18 -0
  24. package/src/oauth/form.spec.ts +35 -0
  25. package/src/oauth/form.ts +30 -0
  26. package/src/oauth/idToken.spec.ts +191 -0
  27. package/src/oauth/idToken.ts +153 -0
  28. package/src/oauth/index.ts +10 -0
  29. package/src/oauth/jwt.ts +90 -0
  30. package/src/oauth/loopbackFlow.spec.ts +226 -0
  31. package/src/oauth/loopbackFlow.ts +114 -0
  32. package/src/oauth/pkce.spec.ts +60 -0
  33. package/src/oauth/pkce.ts +80 -0
  34. package/src/oauth/tokenExchange.spec.ts +331 -0
  35. package/src/oauth/tokenExchange.ts +232 -0
  36. package/src/one.ts +30 -3
  37. package/src/provider/AfterAuth.tsx +7 -0
  38. package/src/provider/authProvider/index.native.tsx +37 -4
  39. package/src/session/index.ts +2 -2
  40. package/src/state.ts +5 -2
  41. package/types/betterAuth/expoAuthFlow.native.d.ts +50 -3
  42. package/types/betterAuth/expoAuthFlow.native.d.ts.map +1 -1
  43. package/types/frappeToken.d.ts +14 -1
  44. package/types/frappeToken.d.ts.map +1 -1
  45. package/types/frappeToken.native.d.ts +16 -2
  46. package/types/frappeToken.native.d.ts.map +1 -1
  47. package/types/index.d.ts +1 -1
  48. package/types/index.d.ts.map +1 -1
  49. package/types/keycloak/index.d.ts.map +1 -1
  50. package/types/keycloak/index.webext.d.ts.map +1 -1
  51. package/types/oauth/authorizationUrl.d.ts +32 -0
  52. package/types/oauth/authorizationUrl.d.ts.map +1 -0
  53. package/types/oauth/callback.d.ts +26 -0
  54. package/types/oauth/callback.d.ts.map +1 -0
  55. package/types/oauth/callbackPage.d.ts +34 -0
  56. package/types/oauth/callbackPage.d.ts.map +1 -0
  57. package/types/oauth/endpoints.d.ts +68 -0
  58. package/types/oauth/endpoints.d.ts.map +1 -0
  59. package/types/oauth/errors.d.ts +17 -0
  60. package/types/oauth/errors.d.ts.map +1 -0
  61. package/types/oauth/form.d.ts +16 -0
  62. package/types/oauth/form.d.ts.map +1 -0
  63. package/types/oauth/idToken.d.ts +83 -0
  64. package/types/oauth/idToken.d.ts.map +1 -0
  65. package/types/oauth/index.d.ts +11 -0
  66. package/types/oauth/index.d.ts.map +1 -0
  67. package/types/oauth/jwt.d.ts +31 -0
  68. package/types/oauth/jwt.d.ts.map +1 -0
  69. package/types/oauth/loopbackFlow.d.ts +64 -0
  70. package/types/oauth/loopbackFlow.d.ts.map +1 -0
  71. package/types/oauth/pkce.d.ts +49 -0
  72. package/types/oauth/pkce.d.ts.map +1 -0
  73. package/types/oauth/tokenExchange.d.ts +89 -0
  74. package/types/oauth/tokenExchange.d.ts.map +1 -0
  75. package/types/one.d.ts +7 -0
  76. package/types/one.d.ts.map +1 -1
  77. package/types/provider/AfterAuth.d.ts.map +1 -1
  78. package/types/provider/authProvider/index.native.d.ts.map +1 -1
  79. package/types/state.d.ts.map +1 -1
package/dist/esm/index.js CHANGED
@@ -29,8 +29,8 @@ function useTokensFromQuery() {
29
29
 
30
30
  // src/state.ts
31
31
  import { createStore, useStore } from "@multiplatform.one/store";
32
- import { isBrowser, isIframe as isIframe2, isServer, isTauri } from "@multiplatform.one/platform";
33
- var persist = isIframe2 || isTauri || !isBrowser && !isServer;
32
+ import { isBrowser, isIframe as isIframe2, isServer } from "@multiplatform.one/platform";
33
+ var persist = isIframe2 || !isBrowser && !isServer;
34
34
  var authStore = createStore(
35
35
  { idToken: "", refreshToken: "", token: "" },
36
36
  { name: "auth", persist }
@@ -116,12 +116,122 @@ function getAuthClient(baseURL) {
116
116
  return _client;
117
117
  }
118
118
 
119
+ // src/frappeToken.ts
120
+ var cached;
121
+ var pending;
122
+ var signedOutUntil = 0;
123
+ var unavailableUntil = 0;
124
+ var SIGNED_OUT_COOLDOWN_MS = 15e3;
125
+ var TRANSIENT_COOLDOWN_MS = 3e3;
126
+ var RETRY_DELAYS_MS = [0, 400, 1e3];
127
+ var FAILED_TO_GET_ACCESS_TOKEN = "FAILED_TO_GET_ACCESS_TOKEN";
128
+ function classifyAccessTokenResponse(status, body) {
129
+ if (status === 401 || status === 403) return "signed-out";
130
+ if (status === 400 && body?.code === FAILED_TO_GET_ACCESS_TOKEN) return "signed-out";
131
+ return "transient";
132
+ }
133
+ function notifySessionInvalidated() {
134
+ try {
135
+ const client = getAuthClient();
136
+ client.$store?.notify?.("$sessionSignal");
137
+ } catch {
138
+ }
139
+ }
140
+ function sleep(ms) {
141
+ return new Promise((resolve) => setTimeout(resolve, ms));
142
+ }
143
+ async function getKeycloakBearerToken() {
144
+ if (typeof window === "undefined") return void 0;
145
+ if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
146
+ if (Date.now() < signedOutUntil) return void 0;
147
+ if (Date.now() < unavailableUntil) return void 0;
148
+ if (pending) return pending;
149
+ pending = fetchToken().finally(() => {
150
+ pending = void 0;
151
+ });
152
+ return pending;
153
+ }
154
+ async function fetchTokenOnce() {
155
+ try {
156
+ const res = await fetch(`${window.location.origin}/api/auth/get-access-token`, {
157
+ method: "POST",
158
+ credentials: "include",
159
+ headers: { "Content-Type": "application/json" },
160
+ body: JSON.stringify({ providerId: "keycloak" })
161
+ });
162
+ if (!res.ok) {
163
+ const body = await res.json().catch(() => null);
164
+ return { ok: false, kind: classifyAccessTokenResponse(res.status, body) };
165
+ }
166
+ const data = await res.json().catch(() => null);
167
+ if (!data?.accessToken) {
168
+ return { ok: false, kind: "signed-out" };
169
+ }
170
+ return {
171
+ ok: true,
172
+ token: data.accessToken,
173
+ expiresAt: data.accessTokenExpiresAt ? new Date(data.accessTokenExpiresAt).getTime() : Date.now() + 6e4
174
+ };
175
+ } catch {
176
+ return { ok: false, kind: "transient" };
177
+ }
178
+ }
179
+ async function fetchToken() {
180
+ let last = "transient";
181
+ for (let i = 0; i < RETRY_DELAYS_MS.length; i++) {
182
+ const wait = RETRY_DELAYS_MS[i];
183
+ if (wait) await sleep(wait);
184
+ const result = await fetchTokenOnce();
185
+ if (result.ok) {
186
+ cached = { token: result.token, expiresAt: result.expiresAt };
187
+ return result.token;
188
+ }
189
+ last = result.kind;
190
+ if (result.kind === "signed-out") {
191
+ cached = void 0;
192
+ signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
193
+ notifySessionInvalidated();
194
+ return void 0;
195
+ }
196
+ }
197
+ cached = void 0;
198
+ if (last === "transient") {
199
+ unavailableUntil = Date.now() + TRANSIENT_COOLDOWN_MS;
200
+ }
201
+ return void 0;
202
+ }
203
+ function clearKeycloakBearerToken() {
204
+ cached = void 0;
205
+ pending = void 0;
206
+ signedOutUntil = 0;
207
+ unavailableUntil = 0;
208
+ }
209
+ function watchKeycloakBearerTokenRevalidate() {
210
+ if (typeof document === "undefined" || typeof window === "undefined") {
211
+ return () => {
212
+ };
213
+ }
214
+ const onVisible = () => {
215
+ if (document.visibilityState !== "visible") return;
216
+ cached = void 0;
217
+ signedOutUntil = 0;
218
+ unavailableUntil = 0;
219
+ void getKeycloakBearerToken();
220
+ };
221
+ document.addEventListener("visibilitychange", onVisible);
222
+ window.addEventListener("focus", onVisible);
223
+ return () => {
224
+ document.removeEventListener("visibilitychange", onVisible);
225
+ window.removeEventListener("focus", onVisible);
226
+ };
227
+ }
228
+
119
229
  // src/session/index.ts
120
- import { isBrowser as isBrowser2, isTauri as isTauri2 } from "@multiplatform.one/platform";
230
+ import { isBrowser as isBrowser2 } from "@multiplatform.one/platform";
121
231
  var useSession = (_options) => {
122
232
  return { session: null, status: "unauthenticated" };
123
233
  };
124
- if (isBrowser2 && !isTauri2) {
234
+ if (isBrowser2) {
125
235
  useSession = (_options) => {
126
236
  const client = getAuthClient();
127
237
  const { data, isPending, error: _error } = client.useSession();
@@ -312,21 +422,29 @@ var Keycloak = class extends BaseKeycloak {
312
422
  async logout(options = {}) {
313
423
  if (this._keycloakClient) {
314
424
  await this._keycloakClient.logout(options);
425
+ } else if (this._logout) {
426
+ await this._logout(options);
427
+ } else if (typeof window !== "undefined" && window.location?.href) {
428
+ clearKeycloakBearerToken();
429
+ const origin = window.location.origin;
430
+ let callbackURL = options.redirectUri || "/";
431
+ try {
432
+ const url = new URL(callbackURL, origin);
433
+ callbackURL = url.origin === origin ? `${url.pathname}${url.search}${url.hash}` || "/" : url.toString();
434
+ } catch {
435
+ callbackURL = "/";
436
+ }
437
+ window.location.href = `${origin}/api/auth/sign-out/sso?callbackURL=${encodeURIComponent(callbackURL)}`;
438
+ return;
315
439
  } else if (isBrowser3 && !isServer2) {
316
440
  const client = getAuthClient();
317
441
  await client.signOut({
318
442
  fetchOptions: {
319
443
  onSuccess: () => {
320
- if (options.redirectUri) {
321
- if (isWeb2) window.location.href = options.redirectUri;
322
- } else {
323
- if (isWeb2) window.location.reload();
324
- }
444
+ if (options.redirectUri && isWeb2) window.location.href = options.redirectUri;
325
445
  }
326
446
  }
327
447
  });
328
- } else {
329
- await this._logout?.(options);
330
448
  }
331
449
  this._clear();
332
450
  }
@@ -440,6 +558,7 @@ function AfterAuth({ children, loadingComponent: _loadingComponent }) {
440
558
  logger.debug("keycloak disabled");
441
559
  }
442
560
  }, [authConfig.debug, keycloak]);
561
+ useEffect2(() => watchKeycloakBearerTokenRevalidate(), []);
443
562
  return /* @__PURE__ */ jsx2(Fragment2, { children });
444
563
  }
445
564
 
@@ -837,52 +956,6 @@ function KeycloakProvider({
837
956
  }
838
957
  ) });
839
958
  }
840
-
841
- // src/frappeToken.ts
842
- var cached;
843
- var pending;
844
- var signedOutUntil = 0;
845
- var SIGNED_OUT_COOLDOWN_MS = 15e3;
846
- async function getKeycloakBearerToken() {
847
- if (typeof window === "undefined") return void 0;
848
- if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
849
- if (Date.now() < signedOutUntil) return void 0;
850
- if (pending) return pending;
851
- pending = fetchToken().finally(() => {
852
- pending = void 0;
853
- });
854
- return pending;
855
- }
856
- async function fetchToken() {
857
- try {
858
- const res = await fetch(`${window.location.origin}/api/auth/get-access-token`, {
859
- method: "POST",
860
- credentials: "include",
861
- headers: { "Content-Type": "application/json" },
862
- body: JSON.stringify({ providerId: "keycloak" })
863
- });
864
- if (!res.ok) {
865
- cached = void 0;
866
- signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
867
- return void 0;
868
- }
869
- const data = await res.json().catch(() => null);
870
- if (!data?.accessToken) {
871
- cached = void 0;
872
- signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
873
- return void 0;
874
- }
875
- cached = {
876
- token: data.accessToken,
877
- expiresAt: data.accessTokenExpiresAt ? new Date(data.accessTokenExpiresAt).getTime() : Date.now() + 6e4
878
- };
879
- return cached.token;
880
- } catch {
881
- cached = void 0;
882
- signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
883
- return void 0;
884
- }
885
- }
886
959
  export {
887
960
  AfterAuth,
888
961
  AuthConfigContext,
@@ -905,5 +978,6 @@ export {
905
978
  useTokensFromQuery,
906
979
  useTokensFromStore,
907
980
  validOrRefreshableToken,
981
+ watchKeycloakBearerTokenRevalidate,
908
982
  withAuthenticated
909
983
  };
@@ -21,8 +21,8 @@ var useTokensFromQuery = () => false;
21
21
 
22
22
  // src/state.ts
23
23
  import { createStore, useStore } from "@multiplatform.one/store";
24
- import { isBrowser, isIframe, isServer, isTauri } from "@multiplatform.one/platform";
25
- var persist = isIframe || isTauri || !isBrowser && !isServer;
24
+ import { isBrowser, isIframe, isServer } from "@multiplatform.one/platform";
25
+ var persist = isIframe || !isBrowser && !isServer;
26
26
  var authStore = createStore(
27
27
  { idToken: "", refreshToken: "", token: "" },
28
28
  { name: "auth", persist }
@@ -285,82 +285,19 @@ function withAuthenticated(Component, options = {}) {
285
285
  // src/provider/AfterAuth.tsx
286
286
  import { logger } from "@multiplatform.one/logger";
287
287
  import { useEffect as useEffect2 } from "react";
288
- import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
289
- function AfterAuth({ children, loadingComponent: _loadingComponent }) {
290
- const authConfig = useAuthConfig();
291
- const authStore2 = useAuthStore();
292
- const keycloak = useKeycloak();
293
- useEffect2(() => {
294
- if (!persist || !keycloak?.authenticated) return;
295
- if (keycloak.token) {
296
- authStore2.setTokens({
297
- token: keycloak.token,
298
- ...keycloak.idToken && { idToken: keycloak.idToken },
299
- ...keycloak.refreshToken && {
300
- refreshToken: keycloak.refreshToken
301
- }
302
- });
303
- }
304
- }, [
305
- authStore2,
306
- persist,
307
- keycloak?.authenticated,
308
- keycloak?.token,
309
- keycloak?.idToken,
310
- keycloak?.refreshToken
311
- ]);
312
- useEffect2(() => {
313
- if (authConfig.debug && keycloak?.token) {
314
- logger.debug("token", keycloak.token);
315
- }
316
- }, [authConfig.debug, keycloak?.token]);
317
- useEffect2(() => {
318
- if (authConfig.debug && keycloak?.idToken) {
319
- logger.debug("idToken", keycloak.idToken);
320
- }
321
- }, [authConfig.debug, keycloak?.idToken]);
322
- useEffect2(() => {
323
- if (authConfig.debug && keycloak?.refreshToken) {
324
- logger.debug("refreshToken", keycloak.refreshToken);
325
- }
326
- }, [authConfig.debug, keycloak?.refreshToken]);
327
- useEffect2(() => {
328
- if (authConfig.debug && keycloak?.authenticated) {
329
- logger.debug("authenticated", keycloak.authenticated);
330
- }
331
- }, [authConfig.debug, keycloak?.authenticated]);
332
- useEffect2(() => {
333
- if (authConfig.debug && keycloak === null) {
334
- logger.debug("keycloak disabled");
335
- }
336
- }, [authConfig.debug, keycloak]);
337
- return /* @__PURE__ */ jsx2(Fragment2, { children });
338
- }
339
-
340
- // src/provider/authProvider/index.native.tsx
341
- import { useEffect as useEffect3, useMemo, useState } from "react";
342
- import * as Linking2 from "expo-linking";
343
288
 
344
- // src/Loading.tsx
345
- import { ActivityIndicator, Text as Text2, View } from "react-native";
346
- import { jsx as jsx3, jsxs } from "react/jsx-runtime";
347
- function Loading({ loadingComponent }) {
348
- const { debug } = useAuthConfig();
349
- const LoadingComponent = loadingComponent;
350
- if (typeof LoadingComponent === "undefined") {
351
- return /* @__PURE__ */ jsxs(View, { style: { flex: 1, alignItems: "center", justifyContent: "center" }, children: [
352
- /* @__PURE__ */ jsx3(ActivityIndicator, {}),
353
- debug ? /* @__PURE__ */ jsx3(Text2, { children: "loading" }) : null
354
- ] });
355
- }
356
- return /* @__PURE__ */ jsx3(LoadingComponent, {});
357
- }
289
+ // src/frappeToken.native.ts
290
+ import { AppState } from "react-native";
358
291
 
359
292
  // src/betterAuth/expoAuthFlow.native.ts
360
293
  import { Platform } from "react-native";
361
294
  import * as Linking from "expo-linking";
362
295
  import * as SecureStore from "expo-secure-store";
363
296
  import * as WebBrowser from "expo-web-browser";
297
+ try {
298
+ WebBrowser.maybeCompleteAuthSession?.();
299
+ } catch {
300
+ }
364
301
  var COOKIE_KEY = "better-auth_cookie";
365
302
  var SECURE_COOKIE_PREFIX = "__Secure-";
366
303
  function readJar() {
@@ -420,6 +357,78 @@ function getOAuthState() {
420
357
  }
421
358
  return null;
422
359
  }
360
+ function rewriteLoopbackHostForNative(configured) {
361
+ const deviceHost = Platform.OS === "android" ? "10.0.2.2" : "127.0.0.1";
362
+ return configured.replace(/^(https?:\/\/)(localhost|127\.0\.0\.1)(?=[:/]|$)/i, `$1${deviceHost}`);
363
+ }
364
+ function normalizeForReturnMatch(url) {
365
+ return url.split("?")[0].split("#")[0].replace(/:\/{2,}/, "://").replace(/\/+$/, "");
366
+ }
367
+ function ensureReturnUrlAuthority(url, segment) {
368
+ return url.replace(/^([a-z0-9.+-]+:)\/{2,3}(?=\?|#|$)/i, `$1//${segment}`);
369
+ }
370
+ function extractCookieFromReturnUrl(returnUrl) {
371
+ const q = returnUrl.indexOf("?");
372
+ if (q === -1) return null;
373
+ const query = returnUrl.slice(q + 1).split("#")[0];
374
+ for (const part of query.split("&")) {
375
+ const eq = part.indexOf("=");
376
+ const key = eq === -1 ? part : part.slice(0, eq);
377
+ let decodedKey;
378
+ try {
379
+ decodedKey = decodeURIComponent(key.replace(/\+/g, " "));
380
+ } catch {
381
+ decodedKey = key;
382
+ }
383
+ if (decodedKey !== "cookie") continue;
384
+ const raw = eq === -1 ? "" : part.slice(eq + 1);
385
+ try {
386
+ return decodeURIComponent(raw.replace(/\+/g, " "));
387
+ } catch {
388
+ return raw;
389
+ }
390
+ }
391
+ return null;
392
+ }
393
+ async function openAuthBrowser(url, returnUrlBase, options) {
394
+ const returnBase = normalizeForReturnMatch(returnUrlBase);
395
+ let returned;
396
+ const linkSub = Linking.addEventListener("url", ({ url: next }) => {
397
+ if (!next || returned) return;
398
+ if (!normalizeForReturnMatch(next).startsWith(returnBase)) return;
399
+ returned = next;
400
+ try {
401
+ WebBrowser.dismissAuthSession();
402
+ } catch {
403
+ }
404
+ });
405
+ const timer = options?.timeoutMs ? setTimeout(() => {
406
+ if (returned) return;
407
+ returned = "";
408
+ try {
409
+ WebBrowser.dismissAuthSession();
410
+ } catch {
411
+ }
412
+ }, options.timeoutMs) : void 0;
413
+ try {
414
+ const result = await WebBrowser.openAuthSessionAsync(url, returnUrlBase, {
415
+ preferEphemeralSession: false
416
+ });
417
+ if (!returned && result.type === "success" && "url" in result && result.url) {
418
+ returned = result.url;
419
+ }
420
+ if (!returned) {
421
+ await new Promise((resolve) => setTimeout(resolve, 100));
422
+ }
423
+ return returned || void 0;
424
+ } finally {
425
+ if (timer) clearTimeout(timer);
426
+ try {
427
+ linkSub.remove();
428
+ } catch {
429
+ }
430
+ }
431
+ }
423
432
  var activeFlow = null;
424
433
  function getActiveExpoAuthFlow() {
425
434
  return activeFlow;
@@ -461,45 +470,32 @@ function createExpoAuthFlow(baseURL, scheme) {
461
470
  if (oauthState) params.append("oauthState", oauthState);
462
471
  const proxyURL = `${base}/api/auth/expo-authorization-proxy?${params.toString()}`;
463
472
  const toBase = to.split("?")[0];
464
- let returnUrl;
465
- if (Platform.OS === "ios") {
466
- returnUrl = await new Promise((resolve, reject) => {
467
- let settled = false;
468
- const linkSub = Linking.addEventListener("url", ({ url }) => {
469
- if (settled || !url || !url.startsWith(toBase)) return;
470
- settled = true;
471
- try {
472
- linkSub.remove();
473
- } catch {
474
- }
475
- resolve(url);
476
- });
477
- Linking.openURL(proxyURL).catch((e) => {
478
- if (settled) return;
479
- settled = true;
480
- try {
481
- linkSub.remove();
482
- } catch {
483
- }
484
- reject(e instanceof Error ? e : new Error(`sign-in openURL failed: ${e}`));
485
- });
486
- });
487
- } else {
488
- const result = await WebBrowser.openAuthSessionAsync(proxyURL, toBase, {
489
- preferEphemeralSession: false
490
- // keep the SSO + siww nonce cookie across the wallet bounce
491
- });
492
- returnUrl = result.type === "success" && "url" in result ? result.url : void 0;
493
- }
473
+ const returnUrl = await openAuthBrowser(proxyURL, toBase);
494
474
  if (!returnUrl) {
495
475
  return;
496
476
  }
497
- const cookie = new URL(returnUrl).searchParams.get("cookie");
477
+ const cookie = extractCookieFromReturnUrl(returnUrl);
498
478
  if (cookie) absorbSetCookie(cookie);
499
479
  },
500
- async signOut() {
480
+ async signOut(postLogoutRedirectUri) {
481
+ const to = ensureReturnUrlAuthority(
482
+ postLogoutRedirectUri || Linking.createURL("/"),
483
+ "signed-out"
484
+ );
501
485
  try {
502
- await authFetch("/sign-out", { method: "POST", body: "{}" });
486
+ const res = await authFetch(
487
+ `/sign-out/sso?mode=json&callbackURL=${encodeURIComponent(to)}`,
488
+ { method: "GET" }
489
+ );
490
+ if (!res.ok) throw new Error(`sign-out/sso failed (${res.status})`);
491
+ const data = await res.json().catch(() => null);
492
+ if (data?.redirect && data.url) {
493
+ await openAuthBrowser(data.url, to.split("?")[0], { timeoutMs: 12e4 }).catch(
494
+ () => void 0
495
+ );
496
+ }
497
+ } catch {
498
+ await authFetch("/sign-out", { method: "POST", body: "{}" }).catch(() => void 0);
503
499
  } finally {
504
500
  writeJar({});
505
501
  }
@@ -523,6 +519,131 @@ function createExpoAuthFlow(baseURL, scheme) {
523
519
  return flow;
524
520
  }
525
521
 
522
+ // src/frappeToken.native.ts
523
+ var cached;
524
+ var pending;
525
+ var signedOutUntil = 0;
526
+ var SIGNED_OUT_COOLDOWN_MS = 15e3;
527
+ async function getKeycloakBearerToken() {
528
+ if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
529
+ if (Date.now() < signedOutUntil) return void 0;
530
+ if (pending) return pending;
531
+ pending = fetchToken().finally(() => {
532
+ pending = void 0;
533
+ });
534
+ return pending;
535
+ }
536
+ async function fetchToken() {
537
+ const flow = getActiveExpoAuthFlow();
538
+ if (!flow) return void 0;
539
+ try {
540
+ const res = await flow.getAccessToken("keycloak");
541
+ if (!res?.accessToken) {
542
+ cached = void 0;
543
+ signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
544
+ return void 0;
545
+ }
546
+ cached = {
547
+ token: res.accessToken,
548
+ expiresAt: res.accessTokenExpiresAt ? new Date(res.accessTokenExpiresAt).getTime() : Date.now() + 6e4
549
+ };
550
+ return cached.token;
551
+ } catch {
552
+ cached = void 0;
553
+ signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
554
+ return void 0;
555
+ }
556
+ }
557
+ function clearKeycloakBearerToken() {
558
+ cached = void 0;
559
+ pending = void 0;
560
+ signedOutUntil = 0;
561
+ }
562
+ function watchKeycloakBearerTokenRevalidate() {
563
+ const onChange = (next) => {
564
+ if (next !== "active") return;
565
+ cached = void 0;
566
+ signedOutUntil = 0;
567
+ void getKeycloakBearerToken();
568
+ };
569
+ const sub = AppState.addEventListener("change", onChange);
570
+ return () => sub.remove();
571
+ }
572
+
573
+ // src/provider/AfterAuth.tsx
574
+ import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
575
+ function AfterAuth({ children, loadingComponent: _loadingComponent }) {
576
+ const authConfig = useAuthConfig();
577
+ const authStore2 = useAuthStore();
578
+ const keycloak = useKeycloak();
579
+ useEffect2(() => {
580
+ if (!persist || !keycloak?.authenticated) return;
581
+ if (keycloak.token) {
582
+ authStore2.setTokens({
583
+ token: keycloak.token,
584
+ ...keycloak.idToken && { idToken: keycloak.idToken },
585
+ ...keycloak.refreshToken && {
586
+ refreshToken: keycloak.refreshToken
587
+ }
588
+ });
589
+ }
590
+ }, [
591
+ authStore2,
592
+ persist,
593
+ keycloak?.authenticated,
594
+ keycloak?.token,
595
+ keycloak?.idToken,
596
+ keycloak?.refreshToken
597
+ ]);
598
+ useEffect2(() => {
599
+ if (authConfig.debug && keycloak?.token) {
600
+ logger.debug("token", keycloak.token);
601
+ }
602
+ }, [authConfig.debug, keycloak?.token]);
603
+ useEffect2(() => {
604
+ if (authConfig.debug && keycloak?.idToken) {
605
+ logger.debug("idToken", keycloak.idToken);
606
+ }
607
+ }, [authConfig.debug, keycloak?.idToken]);
608
+ useEffect2(() => {
609
+ if (authConfig.debug && keycloak?.refreshToken) {
610
+ logger.debug("refreshToken", keycloak.refreshToken);
611
+ }
612
+ }, [authConfig.debug, keycloak?.refreshToken]);
613
+ useEffect2(() => {
614
+ if (authConfig.debug && keycloak?.authenticated) {
615
+ logger.debug("authenticated", keycloak.authenticated);
616
+ }
617
+ }, [authConfig.debug, keycloak?.authenticated]);
618
+ useEffect2(() => {
619
+ if (authConfig.debug && keycloak === null) {
620
+ logger.debug("keycloak disabled");
621
+ }
622
+ }, [authConfig.debug, keycloak]);
623
+ useEffect2(() => watchKeycloakBearerTokenRevalidate(), []);
624
+ return /* @__PURE__ */ jsx2(Fragment2, { children });
625
+ }
626
+
627
+ // src/provider/authProvider/index.native.tsx
628
+ import { useEffect as useEffect3, useMemo, useState } from "react";
629
+ import { AppState as AppState2 } from "react-native";
630
+ import * as Linking2 from "expo-linking";
631
+
632
+ // src/Loading.tsx
633
+ import { ActivityIndicator, Text as Text2, View } from "react-native";
634
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
635
+ function Loading({ loadingComponent }) {
636
+ const { debug } = useAuthConfig();
637
+ const LoadingComponent = loadingComponent;
638
+ if (typeof LoadingComponent === "undefined") {
639
+ return /* @__PURE__ */ jsxs(View, { style: { flex: 1, alignItems: "center", justifyContent: "center" }, children: [
640
+ /* @__PURE__ */ jsx3(ActivityIndicator, {}),
641
+ debug ? /* @__PURE__ */ jsx3(Text2, { children: "loading" }) : null
642
+ ] });
643
+ }
644
+ return /* @__PURE__ */ jsx3(LoadingComponent, {});
645
+ }
646
+
526
647
  // src/session/index.native.ts
527
648
  import { useSyncExternalStore } from "react";
528
649
  var state = { session: null, status: "loading" };
@@ -560,7 +681,9 @@ function AuthProvider({
560
681
  const [keycloak, setKeycloak] = useState();
561
682
  const [isLoading, setIsLoading] = useState(true);
562
683
  const serverBaseUrl = useMemo(() => {
563
- return keycloakConfig.betterAuthBaseUrl || "http://localhost:8000";
684
+ return rewriteLoopbackHostForNative(
685
+ keycloakConfig.betterAuthBaseUrl || "http://localhost:8000"
686
+ );
564
687
  }, [keycloakConfig]);
565
688
  const scheme = useMemo(() => keycloakConfig.expoScheme, [keycloakConfig]);
566
689
  const flow = useMemo(
@@ -587,11 +710,14 @@ function AuthProvider({
587
710
  void 0,
588
711
  async (options) => {
589
712
  await flow.signIn("keycloak", options.redirectUri || Linking2.createURL("/"));
713
+ clearKeycloakBearerToken();
590
714
  await refresh();
591
715
  return void 0;
592
716
  },
593
- async (_options) => {
594
- await flow.signOut();
717
+ async (options) => {
718
+ const redirectUri = options.redirectUri && options.redirectUri.includes("://") ? options.redirectUri : Linking2.createURL("/");
719
+ await flow.signOut(redirectUri);
720
+ clearKeycloakBearerToken();
595
721
  await refresh();
596
722
  return void 0;
597
723
  }
@@ -617,8 +743,13 @@ function AuthProvider({
617
743
  refresh().finally(() => {
618
744
  if (alive) setIsLoading(false);
619
745
  });
746
+ const onAppState = (next) => {
747
+ if (next === "active") refresh().catch(() => void 0);
748
+ };
749
+ const appSub = AppState2.addEventListener("change", onAppState);
620
750
  return () => {
621
751
  alive = false;
752
+ appSub.remove();
622
753
  };
623
754
  }, [disabled, flow, keycloakConfig]);
624
755
  if (disabled) return /* @__PURE__ */ jsx4(Fragment3, { children });
@@ -664,29 +795,6 @@ function KeycloakProvider({
664
795
  }
665
796
  ) });
666
797
  }
667
-
668
- // src/frappeToken.native.ts
669
- var cached;
670
- async function getKeycloakBearerToken() {
671
- if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
672
- const flow = getActiveExpoAuthFlow();
673
- if (!flow) return void 0;
674
- try {
675
- const res = await flow.getAccessToken("keycloak");
676
- if (!res?.accessToken) {
677
- cached = void 0;
678
- return void 0;
679
- }
680
- cached = {
681
- token: res.accessToken,
682
- expiresAt: res.accessTokenExpiresAt ? new Date(res.accessTokenExpiresAt).getTime() : Date.now() + 6e4
683
- };
684
- return cached.token;
685
- } catch {
686
- cached = void 0;
687
- return void 0;
688
- }
689
- }
690
798
  export {
691
799
  AfterAuth,
692
800
  AuthConfigContext,
@@ -711,5 +819,6 @@ export {
711
819
  useTokensFromQuery,
712
820
  useTokensFromStore,
713
821
  validOrRefreshableToken,
822
+ watchKeycloakBearerTokenRevalidate,
714
823
  withAuthenticated
715
824
  };