@multiplatform.one/keycloak 6.7.0 → 7.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.
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
@@ -51,6 +51,7 @@ __export(index_exports, {
51
51
  useTokensFromQuery: () => useTokensFromQuery,
52
52
  useTokensFromStore: () => useTokensFromStore,
53
53
  validOrRefreshableToken: () => validOrRefreshableToken,
54
+ watchKeycloakBearerTokenRevalidate: () => watchKeycloakBearerTokenRevalidate,
54
55
  withAuthenticated: () => withAuthenticated
55
56
  });
56
57
  module.exports = __toCommonJS(index_exports);
@@ -87,7 +88,7 @@ function useTokensFromQuery() {
87
88
  // src/state.ts
88
89
  var import_store = require("@multiplatform.one/store");
89
90
  var import_platform2 = require("@multiplatform.one/platform");
90
- var persist = import_platform2.isIframe || import_platform2.isTauri || !import_platform2.isBrowser && !import_platform2.isServer;
91
+ var persist = import_platform2.isIframe || !import_platform2.isBrowser && !import_platform2.isServer;
91
92
  var authStore = (0, import_store.createStore)(
92
93
  { idToken: "", refreshToken: "", token: "" },
93
94
  { name: "auth", persist }
@@ -173,12 +174,122 @@ function getAuthClient(baseURL) {
173
174
  return _client;
174
175
  }
175
176
 
177
+ // src/frappeToken.ts
178
+ var cached;
179
+ var pending;
180
+ var signedOutUntil = 0;
181
+ var unavailableUntil = 0;
182
+ var SIGNED_OUT_COOLDOWN_MS = 15e3;
183
+ var TRANSIENT_COOLDOWN_MS = 3e3;
184
+ var RETRY_DELAYS_MS = [0, 400, 1e3];
185
+ var FAILED_TO_GET_ACCESS_TOKEN = "FAILED_TO_GET_ACCESS_TOKEN";
186
+ function classifyAccessTokenResponse(status, body) {
187
+ if (status === 401 || status === 403) return "signed-out";
188
+ if (status === 400 && body?.code === FAILED_TO_GET_ACCESS_TOKEN) return "signed-out";
189
+ return "transient";
190
+ }
191
+ function notifySessionInvalidated() {
192
+ try {
193
+ const client = getAuthClient();
194
+ client.$store?.notify?.("$sessionSignal");
195
+ } catch {
196
+ }
197
+ }
198
+ function sleep(ms) {
199
+ return new Promise((resolve) => setTimeout(resolve, ms));
200
+ }
201
+ async function getKeycloakBearerToken() {
202
+ if (typeof window === "undefined") return void 0;
203
+ if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
204
+ if (Date.now() < signedOutUntil) return void 0;
205
+ if (Date.now() < unavailableUntil) return void 0;
206
+ if (pending) return pending;
207
+ pending = fetchToken().finally(() => {
208
+ pending = void 0;
209
+ });
210
+ return pending;
211
+ }
212
+ async function fetchTokenOnce() {
213
+ try {
214
+ const res = await fetch(`${window.location.origin}/api/auth/get-access-token`, {
215
+ method: "POST",
216
+ credentials: "include",
217
+ headers: { "Content-Type": "application/json" },
218
+ body: JSON.stringify({ providerId: "keycloak" })
219
+ });
220
+ if (!res.ok) {
221
+ const body = await res.json().catch(() => null);
222
+ return { ok: false, kind: classifyAccessTokenResponse(res.status, body) };
223
+ }
224
+ const data = await res.json().catch(() => null);
225
+ if (!data?.accessToken) {
226
+ return { ok: false, kind: "signed-out" };
227
+ }
228
+ return {
229
+ ok: true,
230
+ token: data.accessToken,
231
+ expiresAt: data.accessTokenExpiresAt ? new Date(data.accessTokenExpiresAt).getTime() : Date.now() + 6e4
232
+ };
233
+ } catch {
234
+ return { ok: false, kind: "transient" };
235
+ }
236
+ }
237
+ async function fetchToken() {
238
+ let last = "transient";
239
+ for (let i = 0; i < RETRY_DELAYS_MS.length; i++) {
240
+ const wait = RETRY_DELAYS_MS[i];
241
+ if (wait) await sleep(wait);
242
+ const result = await fetchTokenOnce();
243
+ if (result.ok) {
244
+ cached = { token: result.token, expiresAt: result.expiresAt };
245
+ return result.token;
246
+ }
247
+ last = result.kind;
248
+ if (result.kind === "signed-out") {
249
+ cached = void 0;
250
+ signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
251
+ notifySessionInvalidated();
252
+ return void 0;
253
+ }
254
+ }
255
+ cached = void 0;
256
+ if (last === "transient") {
257
+ unavailableUntil = Date.now() + TRANSIENT_COOLDOWN_MS;
258
+ }
259
+ return void 0;
260
+ }
261
+ function clearKeycloakBearerToken() {
262
+ cached = void 0;
263
+ pending = void 0;
264
+ signedOutUntil = 0;
265
+ unavailableUntil = 0;
266
+ }
267
+ function watchKeycloakBearerTokenRevalidate() {
268
+ if (typeof document === "undefined" || typeof window === "undefined") {
269
+ return () => {
270
+ };
271
+ }
272
+ const onVisible = () => {
273
+ if (document.visibilityState !== "visible") return;
274
+ cached = void 0;
275
+ signedOutUntil = 0;
276
+ unavailableUntil = 0;
277
+ void getKeycloakBearerToken();
278
+ };
279
+ document.addEventListener("visibilitychange", onVisible);
280
+ window.addEventListener("focus", onVisible);
281
+ return () => {
282
+ document.removeEventListener("visibilitychange", onVisible);
283
+ window.removeEventListener("focus", onVisible);
284
+ };
285
+ }
286
+
176
287
  // src/session/index.ts
177
288
  var import_platform4 = require("@multiplatform.one/platform");
178
289
  var useSession = (_options) => {
179
290
  return { session: null, status: "unauthenticated" };
180
291
  };
181
- if (import_platform4.isBrowser && !import_platform4.isTauri) {
292
+ if (import_platform4.isBrowser) {
182
293
  useSession = (_options) => {
183
294
  const client = getAuthClient();
184
295
  const { data, isPending, error: _error } = client.useSession();
@@ -369,21 +480,29 @@ var Keycloak = class extends BaseKeycloak {
369
480
  async logout(options = {}) {
370
481
  if (this._keycloakClient) {
371
482
  await this._keycloakClient.logout(options);
483
+ } else if (this._logout) {
484
+ await this._logout(options);
485
+ } else if (typeof window !== "undefined" && window.location?.href) {
486
+ clearKeycloakBearerToken();
487
+ const origin = window.location.origin;
488
+ let callbackURL = options.redirectUri || "/";
489
+ try {
490
+ const url = new URL(callbackURL, origin);
491
+ callbackURL = url.origin === origin ? `${url.pathname}${url.search}${url.hash}` || "/" : url.toString();
492
+ } catch {
493
+ callbackURL = "/";
494
+ }
495
+ window.location.href = `${origin}/api/auth/sign-out/sso?callbackURL=${encodeURIComponent(callbackURL)}`;
496
+ return;
372
497
  } else if (import_platform5.isBrowser && !import_platform5.isServer) {
373
498
  const client = getAuthClient();
374
499
  await client.signOut({
375
500
  fetchOptions: {
376
501
  onSuccess: () => {
377
- if (options.redirectUri) {
378
- if (import_platform5.isWeb) window.location.href = options.redirectUri;
379
- } else {
380
- if (import_platform5.isWeb) window.location.reload();
381
- }
502
+ if (options.redirectUri && import_platform5.isWeb) window.location.href = options.redirectUri;
382
503
  }
383
504
  }
384
505
  });
385
- } else {
386
- await this._logout?.(options);
387
506
  }
388
507
  this._clear();
389
508
  }
@@ -497,6 +616,7 @@ function AfterAuth({ children, loadingComponent: _loadingComponent }) {
497
616
  import_logger.logger.debug("keycloak disabled");
498
617
  }
499
618
  }, [authConfig.debug, keycloak]);
619
+ (0, import_react9.useEffect)(() => watchKeycloakBearerTokenRevalidate(), []);
500
620
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
501
621
  }
502
622
 
@@ -894,49 +1014,3 @@ function KeycloakProvider({
894
1014
  }
895
1015
  ) });
896
1016
  }
897
-
898
- // src/frappeToken.ts
899
- var cached;
900
- var pending;
901
- var signedOutUntil = 0;
902
- var SIGNED_OUT_COOLDOWN_MS = 15e3;
903
- async function getKeycloakBearerToken() {
904
- if (typeof window === "undefined") return void 0;
905
- if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
906
- if (Date.now() < signedOutUntil) return void 0;
907
- if (pending) return pending;
908
- pending = fetchToken().finally(() => {
909
- pending = void 0;
910
- });
911
- return pending;
912
- }
913
- async function fetchToken() {
914
- try {
915
- const res = await fetch(`${window.location.origin}/api/auth/get-access-token`, {
916
- method: "POST",
917
- credentials: "include",
918
- headers: { "Content-Type": "application/json" },
919
- body: JSON.stringify({ providerId: "keycloak" })
920
- });
921
- if (!res.ok) {
922
- cached = void 0;
923
- signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
924
- return void 0;
925
- }
926
- const data = await res.json().catch(() => null);
927
- if (!data?.accessToken) {
928
- cached = void 0;
929
- signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
930
- return void 0;
931
- }
932
- cached = {
933
- token: data.accessToken,
934
- expiresAt: data.accessTokenExpiresAt ? new Date(data.accessTokenExpiresAt).getTime() : Date.now() + 6e4
935
- };
936
- return cached.token;
937
- } catch {
938
- cached = void 0;
939
- signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
940
- return void 0;
941
- }
942
- }
@@ -53,6 +53,7 @@ __export(index_exports, {
53
53
  useTokensFromQuery: () => useTokensFromQuery,
54
54
  useTokensFromStore: () => useTokensFromStore,
55
55
  validOrRefreshableToken: () => validOrRefreshableToken,
56
+ watchKeycloakBearerTokenRevalidate: () => watchKeycloakBearerTokenRevalidate,
56
57
  withAuthenticated: () => withAuthenticated
57
58
  });
58
59
  module.exports = __toCommonJS(index_exports);
@@ -81,7 +82,7 @@ var useTokensFromQuery = () => false;
81
82
  // src/state.ts
82
83
  var import_store = require("@multiplatform.one/store");
83
84
  var import_platform = require("@multiplatform.one/platform");
84
- var persist = import_platform.isIframe || import_platform.isTauri || !import_platform.isBrowser && !import_platform.isServer;
85
+ var persist = import_platform.isIframe || !import_platform.isBrowser && !import_platform.isServer;
85
86
  var authStore = (0, import_store.createStore)(
86
87
  { idToken: "", refreshToken: "", token: "" },
87
88
  { name: "auth", persist }
@@ -344,82 +345,19 @@ function withAuthenticated(Component, options = {}) {
344
345
  // src/provider/AfterAuth.tsx
345
346
  var import_logger = require("@multiplatform.one/logger");
346
347
  var import_react8 = require("react");
347
- var import_jsx_runtime2 = require("react/jsx-runtime");
348
- function AfterAuth({ children, loadingComponent: _loadingComponent }) {
349
- const authConfig = useAuthConfig();
350
- const authStore2 = useAuthStore();
351
- const keycloak = useKeycloak();
352
- (0, import_react8.useEffect)(() => {
353
- if (!persist || !keycloak?.authenticated) return;
354
- if (keycloak.token) {
355
- authStore2.setTokens({
356
- token: keycloak.token,
357
- ...keycloak.idToken && { idToken: keycloak.idToken },
358
- ...keycloak.refreshToken && {
359
- refreshToken: keycloak.refreshToken
360
- }
361
- });
362
- }
363
- }, [
364
- authStore2,
365
- persist,
366
- keycloak?.authenticated,
367
- keycloak?.token,
368
- keycloak?.idToken,
369
- keycloak?.refreshToken
370
- ]);
371
- (0, import_react8.useEffect)(() => {
372
- if (authConfig.debug && keycloak?.token) {
373
- import_logger.logger.debug("token", keycloak.token);
374
- }
375
- }, [authConfig.debug, keycloak?.token]);
376
- (0, import_react8.useEffect)(() => {
377
- if (authConfig.debug && keycloak?.idToken) {
378
- import_logger.logger.debug("idToken", keycloak.idToken);
379
- }
380
- }, [authConfig.debug, keycloak?.idToken]);
381
- (0, import_react8.useEffect)(() => {
382
- if (authConfig.debug && keycloak?.refreshToken) {
383
- import_logger.logger.debug("refreshToken", keycloak.refreshToken);
384
- }
385
- }, [authConfig.debug, keycloak?.refreshToken]);
386
- (0, import_react8.useEffect)(() => {
387
- if (authConfig.debug && keycloak?.authenticated) {
388
- import_logger.logger.debug("authenticated", keycloak.authenticated);
389
- }
390
- }, [authConfig.debug, keycloak?.authenticated]);
391
- (0, import_react8.useEffect)(() => {
392
- if (authConfig.debug && keycloak === null) {
393
- import_logger.logger.debug("keycloak disabled");
394
- }
395
- }, [authConfig.debug, keycloak]);
396
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
397
- }
398
348
 
399
- // src/provider/authProvider/index.native.tsx
400
- var import_react10 = require("react");
401
- var Linking2 = __toESM(require("expo-linking"));
402
-
403
- // src/Loading.tsx
404
- var import_react_native2 = require("react-native");
405
- var import_jsx_runtime3 = require("react/jsx-runtime");
406
- function Loading({ loadingComponent }) {
407
- const { debug } = useAuthConfig();
408
- const LoadingComponent = loadingComponent;
409
- if (typeof LoadingComponent === "undefined") {
410
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { style: { flex: 1, alignItems: "center", justifyContent: "center" }, children: [
411
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native2.ActivityIndicator, {}),
412
- debug ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native2.Text, { children: "loading" }) : null
413
- ] });
414
- }
415
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LoadingComponent, {});
416
- }
349
+ // src/frappeToken.native.ts
350
+ var import_react_native3 = require("react-native");
417
351
 
418
352
  // src/betterAuth/expoAuthFlow.native.ts
419
- var import_react_native3 = require("react-native");
353
+ var import_react_native2 = require("react-native");
420
354
  var Linking = __toESM(require("expo-linking"));
421
355
  var SecureStore = __toESM(require("expo-secure-store"));
422
356
  var WebBrowser = __toESM(require("expo-web-browser"));
357
+ try {
358
+ WebBrowser.maybeCompleteAuthSession?.();
359
+ } catch {
360
+ }
423
361
  var COOKIE_KEY = "better-auth_cookie";
424
362
  var SECURE_COOKIE_PREFIX = "__Secure-";
425
363
  function readJar() {
@@ -479,6 +417,78 @@ function getOAuthState() {
479
417
  }
480
418
  return null;
481
419
  }
420
+ function rewriteLoopbackHostForNative(configured) {
421
+ const deviceHost = import_react_native2.Platform.OS === "android" ? "10.0.2.2" : "127.0.0.1";
422
+ return configured.replace(/^(https?:\/\/)(localhost|127\.0\.0\.1)(?=[:/]|$)/i, `$1${deviceHost}`);
423
+ }
424
+ function normalizeForReturnMatch(url) {
425
+ return url.split("?")[0].split("#")[0].replace(/:\/{2,}/, "://").replace(/\/+$/, "");
426
+ }
427
+ function ensureReturnUrlAuthority(url, segment) {
428
+ return url.replace(/^([a-z0-9.+-]+:)\/{2,3}(?=\?|#|$)/i, `$1//${segment}`);
429
+ }
430
+ function extractCookieFromReturnUrl(returnUrl) {
431
+ const q = returnUrl.indexOf("?");
432
+ if (q === -1) return null;
433
+ const query = returnUrl.slice(q + 1).split("#")[0];
434
+ for (const part of query.split("&")) {
435
+ const eq = part.indexOf("=");
436
+ const key = eq === -1 ? part : part.slice(0, eq);
437
+ let decodedKey;
438
+ try {
439
+ decodedKey = decodeURIComponent(key.replace(/\+/g, " "));
440
+ } catch {
441
+ decodedKey = key;
442
+ }
443
+ if (decodedKey !== "cookie") continue;
444
+ const raw = eq === -1 ? "" : part.slice(eq + 1);
445
+ try {
446
+ return decodeURIComponent(raw.replace(/\+/g, " "));
447
+ } catch {
448
+ return raw;
449
+ }
450
+ }
451
+ return null;
452
+ }
453
+ async function openAuthBrowser(url, returnUrlBase, options) {
454
+ const returnBase = normalizeForReturnMatch(returnUrlBase);
455
+ let returned;
456
+ const linkSub = Linking.addEventListener("url", ({ url: next }) => {
457
+ if (!next || returned) return;
458
+ if (!normalizeForReturnMatch(next).startsWith(returnBase)) return;
459
+ returned = next;
460
+ try {
461
+ WebBrowser.dismissAuthSession();
462
+ } catch {
463
+ }
464
+ });
465
+ const timer = options?.timeoutMs ? setTimeout(() => {
466
+ if (returned) return;
467
+ returned = "";
468
+ try {
469
+ WebBrowser.dismissAuthSession();
470
+ } catch {
471
+ }
472
+ }, options.timeoutMs) : void 0;
473
+ try {
474
+ const result = await WebBrowser.openAuthSessionAsync(url, returnUrlBase, {
475
+ preferEphemeralSession: false
476
+ });
477
+ if (!returned && result.type === "success" && "url" in result && result.url) {
478
+ returned = result.url;
479
+ }
480
+ if (!returned) {
481
+ await new Promise((resolve) => setTimeout(resolve, 100));
482
+ }
483
+ return returned || void 0;
484
+ } finally {
485
+ if (timer) clearTimeout(timer);
486
+ try {
487
+ linkSub.remove();
488
+ } catch {
489
+ }
490
+ }
491
+ }
482
492
  var activeFlow = null;
483
493
  function getActiveExpoAuthFlow() {
484
494
  return activeFlow;
@@ -520,45 +530,32 @@ function createExpoAuthFlow(baseURL, scheme) {
520
530
  if (oauthState) params.append("oauthState", oauthState);
521
531
  const proxyURL = `${base}/api/auth/expo-authorization-proxy?${params.toString()}`;
522
532
  const toBase = to.split("?")[0];
523
- let returnUrl;
524
- if (import_react_native3.Platform.OS === "ios") {
525
- returnUrl = await new Promise((resolve, reject) => {
526
- let settled = false;
527
- const linkSub = Linking.addEventListener("url", ({ url }) => {
528
- if (settled || !url || !url.startsWith(toBase)) return;
529
- settled = true;
530
- try {
531
- linkSub.remove();
532
- } catch {
533
- }
534
- resolve(url);
535
- });
536
- Linking.openURL(proxyURL).catch((e) => {
537
- if (settled) return;
538
- settled = true;
539
- try {
540
- linkSub.remove();
541
- } catch {
542
- }
543
- reject(e instanceof Error ? e : new Error(`sign-in openURL failed: ${e}`));
544
- });
545
- });
546
- } else {
547
- const result = await WebBrowser.openAuthSessionAsync(proxyURL, toBase, {
548
- preferEphemeralSession: false
549
- // keep the SSO + siww nonce cookie across the wallet bounce
550
- });
551
- returnUrl = result.type === "success" && "url" in result ? result.url : void 0;
552
- }
533
+ const returnUrl = await openAuthBrowser(proxyURL, toBase);
553
534
  if (!returnUrl) {
554
535
  return;
555
536
  }
556
- const cookie = new URL(returnUrl).searchParams.get("cookie");
537
+ const cookie = extractCookieFromReturnUrl(returnUrl);
557
538
  if (cookie) absorbSetCookie(cookie);
558
539
  },
559
- async signOut() {
540
+ async signOut(postLogoutRedirectUri) {
541
+ const to = ensureReturnUrlAuthority(
542
+ postLogoutRedirectUri || Linking.createURL("/"),
543
+ "signed-out"
544
+ );
560
545
  try {
561
- await authFetch("/sign-out", { method: "POST", body: "{}" });
546
+ const res = await authFetch(
547
+ `/sign-out/sso?mode=json&callbackURL=${encodeURIComponent(to)}`,
548
+ { method: "GET" }
549
+ );
550
+ if (!res.ok) throw new Error(`sign-out/sso failed (${res.status})`);
551
+ const data = await res.json().catch(() => null);
552
+ if (data?.redirect && data.url) {
553
+ await openAuthBrowser(data.url, to.split("?")[0], { timeoutMs: 12e4 }).catch(
554
+ () => void 0
555
+ );
556
+ }
557
+ } catch {
558
+ await authFetch("/sign-out", { method: "POST", body: "{}" }).catch(() => void 0);
562
559
  } finally {
563
560
  writeJar({});
564
561
  }
@@ -582,6 +579,131 @@ function createExpoAuthFlow(baseURL, scheme) {
582
579
  return flow;
583
580
  }
584
581
 
582
+ // src/frappeToken.native.ts
583
+ var cached;
584
+ var pending;
585
+ var signedOutUntil = 0;
586
+ var SIGNED_OUT_COOLDOWN_MS = 15e3;
587
+ async function getKeycloakBearerToken() {
588
+ if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
589
+ if (Date.now() < signedOutUntil) return void 0;
590
+ if (pending) return pending;
591
+ pending = fetchToken().finally(() => {
592
+ pending = void 0;
593
+ });
594
+ return pending;
595
+ }
596
+ async function fetchToken() {
597
+ const flow = getActiveExpoAuthFlow();
598
+ if (!flow) return void 0;
599
+ try {
600
+ const res = await flow.getAccessToken("keycloak");
601
+ if (!res?.accessToken) {
602
+ cached = void 0;
603
+ signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
604
+ return void 0;
605
+ }
606
+ cached = {
607
+ token: res.accessToken,
608
+ expiresAt: res.accessTokenExpiresAt ? new Date(res.accessTokenExpiresAt).getTime() : Date.now() + 6e4
609
+ };
610
+ return cached.token;
611
+ } catch {
612
+ cached = void 0;
613
+ signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
614
+ return void 0;
615
+ }
616
+ }
617
+ function clearKeycloakBearerToken() {
618
+ cached = void 0;
619
+ pending = void 0;
620
+ signedOutUntil = 0;
621
+ }
622
+ function watchKeycloakBearerTokenRevalidate() {
623
+ const onChange = (next) => {
624
+ if (next !== "active") return;
625
+ cached = void 0;
626
+ signedOutUntil = 0;
627
+ void getKeycloakBearerToken();
628
+ };
629
+ const sub = import_react_native3.AppState.addEventListener("change", onChange);
630
+ return () => sub.remove();
631
+ }
632
+
633
+ // src/provider/AfterAuth.tsx
634
+ var import_jsx_runtime2 = require("react/jsx-runtime");
635
+ function AfterAuth({ children, loadingComponent: _loadingComponent }) {
636
+ const authConfig = useAuthConfig();
637
+ const authStore2 = useAuthStore();
638
+ const keycloak = useKeycloak();
639
+ (0, import_react8.useEffect)(() => {
640
+ if (!persist || !keycloak?.authenticated) return;
641
+ if (keycloak.token) {
642
+ authStore2.setTokens({
643
+ token: keycloak.token,
644
+ ...keycloak.idToken && { idToken: keycloak.idToken },
645
+ ...keycloak.refreshToken && {
646
+ refreshToken: keycloak.refreshToken
647
+ }
648
+ });
649
+ }
650
+ }, [
651
+ authStore2,
652
+ persist,
653
+ keycloak?.authenticated,
654
+ keycloak?.token,
655
+ keycloak?.idToken,
656
+ keycloak?.refreshToken
657
+ ]);
658
+ (0, import_react8.useEffect)(() => {
659
+ if (authConfig.debug && keycloak?.token) {
660
+ import_logger.logger.debug("token", keycloak.token);
661
+ }
662
+ }, [authConfig.debug, keycloak?.token]);
663
+ (0, import_react8.useEffect)(() => {
664
+ if (authConfig.debug && keycloak?.idToken) {
665
+ import_logger.logger.debug("idToken", keycloak.idToken);
666
+ }
667
+ }, [authConfig.debug, keycloak?.idToken]);
668
+ (0, import_react8.useEffect)(() => {
669
+ if (authConfig.debug && keycloak?.refreshToken) {
670
+ import_logger.logger.debug("refreshToken", keycloak.refreshToken);
671
+ }
672
+ }, [authConfig.debug, keycloak?.refreshToken]);
673
+ (0, import_react8.useEffect)(() => {
674
+ if (authConfig.debug && keycloak?.authenticated) {
675
+ import_logger.logger.debug("authenticated", keycloak.authenticated);
676
+ }
677
+ }, [authConfig.debug, keycloak?.authenticated]);
678
+ (0, import_react8.useEffect)(() => {
679
+ if (authConfig.debug && keycloak === null) {
680
+ import_logger.logger.debug("keycloak disabled");
681
+ }
682
+ }, [authConfig.debug, keycloak]);
683
+ (0, import_react8.useEffect)(() => watchKeycloakBearerTokenRevalidate(), []);
684
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
685
+ }
686
+
687
+ // src/provider/authProvider/index.native.tsx
688
+ var import_react10 = require("react");
689
+ var import_react_native5 = require("react-native");
690
+ var Linking2 = __toESM(require("expo-linking"));
691
+
692
+ // src/Loading.tsx
693
+ var import_react_native4 = require("react-native");
694
+ var import_jsx_runtime3 = require("react/jsx-runtime");
695
+ function Loading({ loadingComponent }) {
696
+ const { debug } = useAuthConfig();
697
+ const LoadingComponent = loadingComponent;
698
+ if (typeof LoadingComponent === "undefined") {
699
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native4.View, { style: { flex: 1, alignItems: "center", justifyContent: "center" }, children: [
700
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native4.ActivityIndicator, {}),
701
+ debug ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native4.Text, { children: "loading" }) : null
702
+ ] });
703
+ }
704
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LoadingComponent, {});
705
+ }
706
+
585
707
  // src/session/index.native.ts
586
708
  var import_react9 = require("react");
587
709
  var state = { session: null, status: "loading" };
@@ -619,7 +741,9 @@ function AuthProvider({
619
741
  const [keycloak, setKeycloak] = (0, import_react10.useState)();
620
742
  const [isLoading, setIsLoading] = (0, import_react10.useState)(true);
621
743
  const serverBaseUrl = (0, import_react10.useMemo)(() => {
622
- return keycloakConfig.betterAuthBaseUrl || "http://localhost:8000";
744
+ return rewriteLoopbackHostForNative(
745
+ keycloakConfig.betterAuthBaseUrl || "http://localhost:8000"
746
+ );
623
747
  }, [keycloakConfig]);
624
748
  const scheme = (0, import_react10.useMemo)(() => keycloakConfig.expoScheme, [keycloakConfig]);
625
749
  const flow = (0, import_react10.useMemo)(
@@ -646,11 +770,14 @@ function AuthProvider({
646
770
  void 0,
647
771
  async (options) => {
648
772
  await flow.signIn("keycloak", options.redirectUri || Linking2.createURL("/"));
773
+ clearKeycloakBearerToken();
649
774
  await refresh();
650
775
  return void 0;
651
776
  },
652
- async (_options) => {
653
- await flow.signOut();
777
+ async (options) => {
778
+ const redirectUri = options.redirectUri && options.redirectUri.includes("://") ? options.redirectUri : Linking2.createURL("/");
779
+ await flow.signOut(redirectUri);
780
+ clearKeycloakBearerToken();
654
781
  await refresh();
655
782
  return void 0;
656
783
  }
@@ -676,8 +803,13 @@ function AuthProvider({
676
803
  refresh().finally(() => {
677
804
  if (alive) setIsLoading(false);
678
805
  });
806
+ const onAppState = (next) => {
807
+ if (next === "active") refresh().catch(() => void 0);
808
+ };
809
+ const appSub = import_react_native5.AppState.addEventListener("change", onAppState);
679
810
  return () => {
680
811
  alive = false;
812
+ appSub.remove();
681
813
  };
682
814
  }, [disabled, flow, keycloakConfig]);
683
815
  if (disabled) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children });
@@ -723,26 +855,3 @@ function KeycloakProvider({
723
855
  }
724
856
  ) });
725
857
  }
726
-
727
- // src/frappeToken.native.ts
728
- var cached;
729
- async function getKeycloakBearerToken() {
730
- if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
731
- const flow = getActiveExpoAuthFlow();
732
- if (!flow) return void 0;
733
- try {
734
- const res = await flow.getAccessToken("keycloak");
735
- if (!res?.accessToken) {
736
- cached = void 0;
737
- return void 0;
738
- }
739
- cached = {
740
- token: res.accessToken,
741
- expiresAt: res.accessTokenExpiresAt ? new Date(res.accessTokenExpiresAt).getTime() : Date.now() + 6e4
742
- };
743
- return cached.token;
744
- } catch {
745
- cached = void 0;
746
- return void 0;
747
- }
748
- }