@multiplatform.one/keycloak 7.6.3 → 7.7.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/cjs/index.cjs +346 -53
- package/dist/cjs/index.native.cjs +390 -20
- package/dist/esm/index.js +345 -44
- package/dist/esm/index.native.js +397 -12
- package/package.json +9 -10
- package/src/betterAuth/accountSelection.spec.ts +131 -0
- package/src/betterAuth/accountSelection.ts +99 -0
- package/src/betterAuth/expoAuthFlow.native.ts +18 -6
- package/src/frappeToken.ts +14 -9
- package/types/betterAuth/accountSelection.d.ts +65 -0
- package/types/betterAuth/accountSelection.d.ts.map +1 -0
- package/types/betterAuth/expoAuthFlow.native.d.ts +7 -1
- package/types/betterAuth/expoAuthFlow.native.d.ts.map +1 -1
- package/types/frappeToken.d.ts.map +1 -1
package/dist/esm/index.native.js
CHANGED
|
@@ -1,5 +1,140 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// ../platform/src/platform/platformBase.ts
|
|
9
|
+
import {
|
|
10
|
+
isChrome,
|
|
11
|
+
isClient,
|
|
12
|
+
isServer,
|
|
13
|
+
isTouchable,
|
|
14
|
+
isWeb,
|
|
15
|
+
isWebTouchable,
|
|
16
|
+
isWindowDefined
|
|
17
|
+
} from "@tamagui/constants";
|
|
18
|
+
var platformOrder = [
|
|
19
|
+
"Ios",
|
|
20
|
+
"Android",
|
|
21
|
+
// Gnome outranks Native: the GTK target reports isNative (it is not a
|
|
22
|
+
// browser) but "gnome" is the precise answer callers want.
|
|
23
|
+
"Gnome",
|
|
24
|
+
"Native",
|
|
25
|
+
// Tauri outranks Desktop/Web the same way: the webview host reports isWeb
|
|
26
|
+
// and isDesktop, but "tauri" is the precise answer.
|
|
27
|
+
"Tauri",
|
|
28
|
+
"Desktop",
|
|
29
|
+
"ChromeExtension",
|
|
30
|
+
"FirefoxExtension",
|
|
31
|
+
"WebExtension",
|
|
32
|
+
"Next",
|
|
33
|
+
"Expo",
|
|
34
|
+
"Chrome",
|
|
35
|
+
"Firefox",
|
|
36
|
+
"Web",
|
|
37
|
+
"Browser",
|
|
38
|
+
"Client",
|
|
39
|
+
"Server",
|
|
40
|
+
"Iframe",
|
|
41
|
+
"Storybook"
|
|
42
|
+
];
|
|
43
|
+
var platformBase = {
|
|
44
|
+
isAndroid: false,
|
|
45
|
+
isBrowser: false,
|
|
46
|
+
isChrome,
|
|
47
|
+
isChromeExtension: false,
|
|
48
|
+
isClient,
|
|
49
|
+
isDesktop: false,
|
|
50
|
+
isExpo: false,
|
|
51
|
+
isFirefox: false,
|
|
52
|
+
isFirefoxExtension: false,
|
|
53
|
+
isGnome: false,
|
|
54
|
+
isIframe: false,
|
|
55
|
+
isIos: false,
|
|
56
|
+
isNative: false,
|
|
57
|
+
isNext: false,
|
|
58
|
+
isServer,
|
|
59
|
+
isStorybook: isWindowDefined && typeof window.__STORYBOOK_ADDONS_PREVIEW === "object",
|
|
60
|
+
isTauri: false,
|
|
61
|
+
isTouchable,
|
|
62
|
+
isWeb,
|
|
63
|
+
isWebExtension: false,
|
|
64
|
+
isWebTouchable,
|
|
65
|
+
isWindowDefined,
|
|
66
|
+
preciseName: "unknown",
|
|
67
|
+
broadName: "unknown"
|
|
68
|
+
};
|
|
69
|
+
function getPreciseName(platform2) {
|
|
70
|
+
const flags = platform2;
|
|
71
|
+
for (const name of platformOrder) {
|
|
72
|
+
if (flags[`is${name}`]) {
|
|
73
|
+
return `${name[0].toLowerCase()}${name.slice(1)}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
function getBroadName(platform2) {
|
|
79
|
+
const flags = platform2;
|
|
80
|
+
for (const name of platformOrder.slice().reverse()) {
|
|
81
|
+
if (flags[`is${name}`]) {
|
|
82
|
+
return `${name[0].toLowerCase()}${name.slice(1)}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return "unknown";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ../platform/src/platform/index.ts
|
|
89
|
+
var isIframe = (() => {
|
|
90
|
+
if (!platformBase.isWindowDefined) return false;
|
|
91
|
+
try {
|
|
92
|
+
return window.self !== window.top;
|
|
93
|
+
} catch {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
var isWebExtension = !!(typeof chrome !== "undefined" && !!chrome?.runtime?.id || typeof browser !== "undefined" && !!browser?.runtime?.id);
|
|
98
|
+
var platform = {
|
|
99
|
+
...platformBase,
|
|
100
|
+
isChromeExtension: typeof chrome !== "undefined" && !!chrome?.runtime?.id,
|
|
101
|
+
isFirefoxExtension: typeof browser !== "undefined" && !!browser?.runtime?.id,
|
|
102
|
+
isIframe,
|
|
103
|
+
isWebExtension,
|
|
104
|
+
isBrowser: !!(platformBase.isWeb && platformBase.isWindowDefined && !isWebExtension),
|
|
105
|
+
isNext: platformBase.isWeb && (!platformBase.isWindowDefined || typeof window.__NEXT_DATA__ === "object"),
|
|
106
|
+
isFirefox: platformBase.isWindowDefined && !!(window?.navigator?.userAgent?.toLowerCase().indexOf("firefox") > -1)
|
|
107
|
+
};
|
|
108
|
+
platform.preciseName = getPreciseName(platform);
|
|
109
|
+
platform.broadName = getBroadName(platform);
|
|
110
|
+
|
|
111
|
+
// ../platform/src/index.ts
|
|
112
|
+
var {
|
|
113
|
+
isAndroid,
|
|
114
|
+
isBrowser,
|
|
115
|
+
isChrome: isChrome2,
|
|
116
|
+
isChromeExtension,
|
|
117
|
+
isClient: isClient2,
|
|
118
|
+
isExpo,
|
|
119
|
+
isFirefox,
|
|
120
|
+
isFirefoxExtension,
|
|
121
|
+
isGnome,
|
|
122
|
+
isIframe: isIframe2,
|
|
123
|
+
isIos,
|
|
124
|
+
isNative,
|
|
125
|
+
isNext,
|
|
126
|
+
isServer: isServer2,
|
|
127
|
+
isStorybook,
|
|
128
|
+
isTauri,
|
|
129
|
+
isDesktop,
|
|
130
|
+
isTouchable: isTouchable2,
|
|
131
|
+
isWeb: isWeb2,
|
|
132
|
+
isWebExtension: isWebExtension2,
|
|
133
|
+
isWebTouchable: isWebTouchable2,
|
|
134
|
+
isWindowDefined: isWindowDefined2
|
|
135
|
+
} = platform;
|
|
136
|
+
|
|
1
137
|
// src/Authenticated.tsx
|
|
2
|
-
import { isIframe as isIframe2, isServer as isServer2 } from "@multiplatform.one/platform";
|
|
3
138
|
import { useEffect } from "react";
|
|
4
139
|
import { Text } from "react-native";
|
|
5
140
|
|
|
@@ -19,16 +154,155 @@ function useAuthConfig() {
|
|
|
19
154
|
// src/hooks/useTokensFromQuery/index.native.ts
|
|
20
155
|
var useTokensFromQuery = () => false;
|
|
21
156
|
|
|
157
|
+
// ../store/src/createStore.ts
|
|
158
|
+
import { Store as Store2 } from "@tanstack/store";
|
|
159
|
+
|
|
160
|
+
// ../store/src/persist.ts
|
|
161
|
+
import { Store } from "@tanstack/store";
|
|
162
|
+
function createPersistStore(initialState, persistOptions) {
|
|
163
|
+
const { key, storage: storage2, serialize = JSON.stringify, deserialize = JSON.parse } = persistOptions;
|
|
164
|
+
let hydrated = false;
|
|
165
|
+
const store = new Store(initialState);
|
|
166
|
+
store.subscribe(() => {
|
|
167
|
+
if (hydrated) {
|
|
168
|
+
const serialized = serialize(store.state);
|
|
169
|
+
void Promise.resolve(storage2.setItem(key, serialized));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const rehydrate = async () => {
|
|
173
|
+
const raw = await Promise.resolve(storage2.getItem(key));
|
|
174
|
+
if (raw !== null) {
|
|
175
|
+
const deserialized = deserialize(raw);
|
|
176
|
+
store.setState(() => deserialized);
|
|
177
|
+
}
|
|
178
|
+
hydrated = true;
|
|
179
|
+
};
|
|
180
|
+
const clearPersistedState = async () => {
|
|
181
|
+
await Promise.resolve(storage2.removeItem(key));
|
|
182
|
+
};
|
|
183
|
+
void rehydrate();
|
|
184
|
+
const persistStore = store;
|
|
185
|
+
persistStore.rehydrate = rehydrate;
|
|
186
|
+
persistStore.clearPersistedState = clearPersistedState;
|
|
187
|
+
return persistStore;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ../store/src/storage/index.native.ts
|
|
191
|
+
var defaultLoaders = {
|
|
192
|
+
// Literal require(...) calls on purpose: metro resolves them natively and
|
|
193
|
+
// vxrn/rolldown rewrites literal requires into bundled module inits (a
|
|
194
|
+
// passed-around `require` reference would survive as a bare identifier and
|
|
195
|
+
// crash). Lazy so a genuinely missing native module throws HERE — inside
|
|
196
|
+
// the try/catch — instead of at bundle evaluation.
|
|
197
|
+
loadMMKV: () => __require("react-native-mmkv"),
|
|
198
|
+
loadAsyncStorage: () => __require("@react-native-async-storage/async-storage")
|
|
199
|
+
};
|
|
200
|
+
function resolveNativeStorage(loaders = defaultLoaders) {
|
|
201
|
+
const errors = {};
|
|
202
|
+
try {
|
|
203
|
+
const mmkvModule = loaders.loadMMKV();
|
|
204
|
+
if (typeof mmkvModule?.createMMKV !== "function") {
|
|
205
|
+
throw new Error("no createMMKV() export (react-native-mmkv v4 API expected)");
|
|
206
|
+
}
|
|
207
|
+
const mmkv = mmkvModule.createMMKV();
|
|
208
|
+
return {
|
|
209
|
+
backend: "mmkv",
|
|
210
|
+
errors,
|
|
211
|
+
storage: {
|
|
212
|
+
getItem: (name) => mmkv.getString(name) ?? null,
|
|
213
|
+
setItem: (name, value) => {
|
|
214
|
+
mmkv.set(name, value);
|
|
215
|
+
},
|
|
216
|
+
removeItem: (name) => {
|
|
217
|
+
mmkv.remove(name);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
} catch (err) {
|
|
222
|
+
errors.mmkv = err;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const asyncStorageModule = loaders.loadAsyncStorage();
|
|
226
|
+
const asyncStorage = asyncStorageModule?.default;
|
|
227
|
+
if (!asyncStorage || typeof asyncStorage.getItem !== "function") {
|
|
228
|
+
throw new Error("no usable default export on @react-native-async-storage/async-storage");
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
backend: "async-storage",
|
|
232
|
+
errors,
|
|
233
|
+
storage: {
|
|
234
|
+
getItem: (name) => asyncStorage.getItem(name),
|
|
235
|
+
setItem: (name, value) => asyncStorage.setItem(name, value),
|
|
236
|
+
removeItem: (name) => asyncStorage.removeItem(name)
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
} catch (err) {
|
|
240
|
+
errors.asyncStorage = err;
|
|
241
|
+
}
|
|
242
|
+
const map = /* @__PURE__ */ new Map();
|
|
243
|
+
return {
|
|
244
|
+
backend: "memory",
|
|
245
|
+
errors,
|
|
246
|
+
storage: {
|
|
247
|
+
getItem: (name) => map.get(name) ?? null,
|
|
248
|
+
setItem: (name, value) => {
|
|
249
|
+
map.set(name, value);
|
|
250
|
+
},
|
|
251
|
+
removeItem: (name) => {
|
|
252
|
+
map.delete(name);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function describeError(err) {
|
|
258
|
+
return err instanceof Error ? err.message : String(err);
|
|
259
|
+
}
|
|
260
|
+
function logNativeStorageResolution(resolution2, logger2 = console) {
|
|
261
|
+
if (resolution2.backend === "mmkv") {
|
|
262
|
+
logger2.info("[@multiplatform.one/store] native storage backend: mmkv");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const causes = [`react-native-mmkv unavailable: ${describeError(resolution2.errors.mmkv)}`];
|
|
266
|
+
if (resolution2.backend === "memory") {
|
|
267
|
+
causes.push(`async-storage unavailable: ${describeError(resolution2.errors.asyncStorage)}`);
|
|
268
|
+
}
|
|
269
|
+
logger2.warn(
|
|
270
|
+
`[@multiplatform.one/store] native storage fell back to ${resolution2.backend}` + (resolution2.backend === "memory" ? " \u2014 persisted state will NOT survive app restarts" : "") + ` (${causes.join("; ")})`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
var resolution = resolveNativeStorage();
|
|
274
|
+
if (process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test") {
|
|
275
|
+
logNativeStorageResolution(resolution);
|
|
276
|
+
}
|
|
277
|
+
var storage = resolution.storage;
|
|
278
|
+
|
|
279
|
+
// ../store/src/createStore.ts
|
|
280
|
+
function createStore(initialState, options) {
|
|
281
|
+
if (options?.persist) {
|
|
282
|
+
return createPersistStore(initialState, {
|
|
283
|
+
key: options.name,
|
|
284
|
+
storage: options.storage ?? storage
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return new Store2(initialState);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ../store/src/index.ts
|
|
291
|
+
import { Store as Store3, batch, createStore as createStore2 } from "@tanstack/store";
|
|
292
|
+
import { useStore } from "@tanstack/react-store";
|
|
293
|
+
import { useStore as tanstackUseStore } from "@tanstack/react-store";
|
|
294
|
+
function useStore2(store, selector) {
|
|
295
|
+
return tanstackUseStore(store, selector ?? ((s) => s));
|
|
296
|
+
}
|
|
297
|
+
|
|
22
298
|
// src/state.ts
|
|
23
|
-
|
|
24
|
-
import { isBrowser, isIframe, isServer } from "@multiplatform.one/platform";
|
|
25
|
-
var persist = isIframe || !isBrowser && !isServer;
|
|
299
|
+
var persist = isIframe2 || !isBrowser && !isServer2;
|
|
26
300
|
var authStore = createStore(
|
|
27
301
|
{ idToken: "", refreshToken: "", token: "" },
|
|
28
302
|
{ name: "auth", persist }
|
|
29
303
|
);
|
|
30
304
|
function useAuthStore() {
|
|
31
|
-
const state2 =
|
|
305
|
+
const state2 = useStore2(authStore);
|
|
32
306
|
return {
|
|
33
307
|
...state2,
|
|
34
308
|
setIdToken: (idToken) => authStore.setState((prev) => ({ ...prev, idToken })),
|
|
@@ -91,7 +365,6 @@ function useTokensFromStore() {
|
|
|
91
365
|
}
|
|
92
366
|
|
|
93
367
|
// src/keycloak/index.native.ts
|
|
94
|
-
import { isStorybook } from "@multiplatform.one/platform";
|
|
95
368
|
import { useContext as useContext3 } from "react";
|
|
96
369
|
|
|
97
370
|
// src/keycloak/base.ts
|
|
@@ -282,8 +555,86 @@ function withAuthenticated(Component, options = {}) {
|
|
|
282
555
|
return (props) => /* @__PURE__ */ jsx(Authenticated, { ...options, children: /* @__PURE__ */ jsx(Component, { ...props }) });
|
|
283
556
|
}
|
|
284
557
|
|
|
558
|
+
// ../logger/src/base.ts
|
|
559
|
+
import log from "loglevel";
|
|
560
|
+
var defaultLoggerConfig = {
|
|
561
|
+
name: "default",
|
|
562
|
+
type: "pretty",
|
|
563
|
+
level: process.env.NODE_ENV === "development" ? 1 /* Debug */ : 2 /* Info */
|
|
564
|
+
};
|
|
565
|
+
var BaseLogger = class {
|
|
566
|
+
constructor(config = {}) {
|
|
567
|
+
const { metadata, name, level } = typeof config === "string" ? { metadata: void 0, name: config, level: void 0 } : config;
|
|
568
|
+
this.metadata = metadata || {};
|
|
569
|
+
this.name = name || "default";
|
|
570
|
+
this.logger = name ? log.getLogger(name) : log;
|
|
571
|
+
if (level !== void 0) this.logger.setLevel(level);
|
|
572
|
+
this.transport = this.createTransport?.();
|
|
573
|
+
}
|
|
574
|
+
trace(...args) {
|
|
575
|
+
this.logger.trace(...args);
|
|
576
|
+
this.sendToTransport(0 /* Trace */, args);
|
|
577
|
+
}
|
|
578
|
+
debug(...args) {
|
|
579
|
+
this.logger.debug(...args);
|
|
580
|
+
this.sendToTransport(1 /* Debug */, args);
|
|
581
|
+
}
|
|
582
|
+
info(...args) {
|
|
583
|
+
this.logger.info(...args);
|
|
584
|
+
this.sendToTransport(2 /* Info */, args);
|
|
585
|
+
}
|
|
586
|
+
warn(...args) {
|
|
587
|
+
this.logger.warn(...args);
|
|
588
|
+
this.sendToTransport(3 /* Warn */, args);
|
|
589
|
+
}
|
|
590
|
+
error(...args) {
|
|
591
|
+
this.logger.error(...args);
|
|
592
|
+
this.sendToTransport(4 /* Error */, args);
|
|
593
|
+
}
|
|
594
|
+
log(...args) {
|
|
595
|
+
this.logger.debug(...args);
|
|
596
|
+
this.sendToTransport(1 /* Debug */, args);
|
|
597
|
+
}
|
|
598
|
+
sendToTransport(level, args) {
|
|
599
|
+
if (this.transport) {
|
|
600
|
+
this.transport.send({
|
|
601
|
+
level,
|
|
602
|
+
message: args[0],
|
|
603
|
+
args: args.slice(1),
|
|
604
|
+
platform: platform.preciseName,
|
|
605
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
getSubLogger(input) {
|
|
610
|
+
const subConfig = typeof input === "string" ? {
|
|
611
|
+
name: input,
|
|
612
|
+
metadata: this.metadata,
|
|
613
|
+
level: this.logger.getLevel()
|
|
614
|
+
} : {
|
|
615
|
+
...input,
|
|
616
|
+
metadata: { ...this.metadata, ...input.metadata }
|
|
617
|
+
};
|
|
618
|
+
return new this.constructor(subConfig);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// ../logger/src/index.ts
|
|
623
|
+
var Logger = class extends BaseLogger {
|
|
624
|
+
constructor(config = {}) {
|
|
625
|
+
const metadata = {
|
|
626
|
+
platform: platform.preciseName
|
|
627
|
+
};
|
|
628
|
+
super({
|
|
629
|
+
name: metadata.platform,
|
|
630
|
+
...typeof config === "string" ? { name: config } : config,
|
|
631
|
+
metadata
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
var logger = new Logger();
|
|
636
|
+
|
|
285
637
|
// src/provider/AfterAuth.tsx
|
|
286
|
-
import { logger } from "@multiplatform.one/logger";
|
|
287
638
|
import { useEffect as useEffect2 } from "react";
|
|
288
639
|
|
|
289
640
|
// src/frappeToken.native.ts
|
|
@@ -294,6 +645,35 @@ import { Platform } from "react-native";
|
|
|
294
645
|
import * as Linking from "expo-linking";
|
|
295
646
|
import * as SecureStore from "expo-secure-store";
|
|
296
647
|
import * as WebBrowser from "expo-web-browser";
|
|
648
|
+
|
|
649
|
+
// src/betterAuth/accountSelection.ts
|
|
650
|
+
var validationErrorCode = "VALIDATION_ERROR";
|
|
651
|
+
var known;
|
|
652
|
+
function currentSelection(userId) {
|
|
653
|
+
return userId ? { useAccountCookie: true, userId } : { useAccountCookie: true };
|
|
654
|
+
}
|
|
655
|
+
function legacySelection(providerId, userId) {
|
|
656
|
+
return userId ? { providerId, userId } : { providerId };
|
|
657
|
+
}
|
|
658
|
+
function isBodyRefusal(res) {
|
|
659
|
+
return res.status === 400 && res.body?.code === validationErrorCode;
|
|
660
|
+
}
|
|
661
|
+
async function requestAccessToken(providerId, send, userId) {
|
|
662
|
+
const current = () => currentSelection(userId);
|
|
663
|
+
const legacy = () => legacySelection(providerId, userId);
|
|
664
|
+
if (known === "legacy") return send(legacy());
|
|
665
|
+
if (known === "current") return send(current());
|
|
666
|
+
const first = await send(current());
|
|
667
|
+
if (!isBodyRefusal(first)) {
|
|
668
|
+
if (first.status !== 400) known = "current";
|
|
669
|
+
return first;
|
|
670
|
+
}
|
|
671
|
+
const second = await send(legacy());
|
|
672
|
+
if (!isBodyRefusal(second)) known = "legacy";
|
|
673
|
+
return second;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/betterAuth/expoAuthFlow.native.ts
|
|
297
677
|
try {
|
|
298
678
|
WebBrowser.maybeCompleteAuthSession?.();
|
|
299
679
|
} catch {
|
|
@@ -507,12 +887,17 @@ function createExpoAuthFlow(baseURL, scheme) {
|
|
|
507
887
|
return data && Object.keys(data).length ? data : null;
|
|
508
888
|
},
|
|
509
889
|
async getAccessToken(providerId) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
890
|
+
let payload = null;
|
|
891
|
+
const res = await requestAccessToken(providerId, async (body) => {
|
|
892
|
+
const response = await authFetch("/get-access-token", {
|
|
893
|
+
method: "POST",
|
|
894
|
+
body: JSON.stringify(body)
|
|
895
|
+
});
|
|
896
|
+
payload = await response.json().catch(() => null);
|
|
897
|
+
return { status: response.status, body: payload };
|
|
513
898
|
});
|
|
514
|
-
if (
|
|
515
|
-
return
|
|
899
|
+
if (res.status < 200 || res.status >= 300) return null;
|
|
900
|
+
return payload;
|
|
516
901
|
}
|
|
517
902
|
};
|
|
518
903
|
activeFlow = flow;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/keycloak",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.7.0",
|
|
4
4
|
"description": "keycloak client for multiplatform.one ecosystem",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"@better-auth/expo": "^1.6.9",
|
|
60
60
|
"better-auth": "^1.6.9",
|
|
61
61
|
"jwt-decode": "^4.0.0",
|
|
62
|
-
"@multiplatform.one/keycloak-js": "7.
|
|
63
|
-
"@multiplatform.one/store": "7.
|
|
62
|
+
"@multiplatform.one/keycloak-js": "7.7.0",
|
|
63
|
+
"@multiplatform.one/store": "7.7.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@tamagui/build": "2.7.6",
|
|
@@ -68,22 +68,21 @@
|
|
|
68
68
|
"@testing-library/react": "^16.3.2",
|
|
69
69
|
"@types/lodash.camelcase": "^4.3.9",
|
|
70
70
|
"@types/lodash.startcase": "^4.4.9",
|
|
71
|
-
"@types/react": "~19.2.14",
|
|
72
71
|
"@vitejs/plugin-react": "^6.0.1",
|
|
73
72
|
"esbuild": "^0.28.0",
|
|
74
73
|
"one": "^1.16.5",
|
|
75
74
|
"react": "19.2.5",
|
|
76
75
|
"typescript": "~5.9.3",
|
|
77
76
|
"vitest": "^4.1.5",
|
|
78
|
-
"@multiplatform.one/config": "7.
|
|
79
|
-
"@multiplatform.one/logger": "7.
|
|
80
|
-
"@multiplatform.one/platform": "7.
|
|
81
|
-
"@multiplatform.one/test-utils": "7.
|
|
77
|
+
"@multiplatform.one/config": "7.7.0",
|
|
78
|
+
"@multiplatform.one/logger": "7.7.0",
|
|
79
|
+
"@multiplatform.one/platform": "7.7.0",
|
|
80
|
+
"@multiplatform.one/test-utils": "7.7.0"
|
|
82
81
|
},
|
|
83
82
|
"peerDependencies": {
|
|
84
83
|
"react": "^19.1.0",
|
|
85
|
-
"@multiplatform.one/logger": "^7.
|
|
86
|
-
"@multiplatform.one/platform": "^7.
|
|
84
|
+
"@multiplatform.one/logger": "^7.7.0",
|
|
85
|
+
"@multiplatform.one/platform": "^7.7.0"
|
|
87
86
|
},
|
|
88
87
|
"optionalDependencies": {
|
|
89
88
|
"expo-linking": "^55.0.15",
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test for the `/get-access-token` request body.
|
|
3
|
+
*
|
|
4
|
+
* This is the test that was missing. Both token clients mocked
|
|
5
|
+
* `getAccessToken` wholesale, so nothing compared the body we send with the
|
|
6
|
+
* body a server accepts — and they silently diverged when Better Auth 1.7
|
|
7
|
+
* replaced `{ providerId }` with a strict account-selection union. Every
|
|
8
|
+
* token fetch then 400'd at validation, the Frappe layer fell back to guest
|
|
9
|
+
* cookies, and a signed-in user was told to sign in.
|
|
10
|
+
*
|
|
11
|
+
* The last case drives the REAL Better Auth router that `createAuth` builds,
|
|
12
|
+
* so the shape is checked against the installed version rather than against a
|
|
13
|
+
* hand-copied schema. A 400 VALIDATION_ERROR fails; 401 is the pass, because
|
|
14
|
+
* the request is unauthenticated by design — the point is that the body
|
|
15
|
+
* reached the auth check at all.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
19
|
+
import { createAuth } from "../one";
|
|
20
|
+
import {
|
|
21
|
+
currentSelection,
|
|
22
|
+
isBodyRefusal,
|
|
23
|
+
legacySelection,
|
|
24
|
+
requestAccessToken,
|
|
25
|
+
resetAccessTokenSelection,
|
|
26
|
+
} from "./accountSelection";
|
|
27
|
+
|
|
28
|
+
const baseUrl = "http://localhost:3000";
|
|
29
|
+
|
|
30
|
+
const refusal = { status: 400, body: { code: "VALIDATION_ERROR" } };
|
|
31
|
+
const unauthorized = { status: 401, body: null };
|
|
32
|
+
const ok = { status: 200, body: null };
|
|
33
|
+
|
|
34
|
+
beforeEach(() => resetAccessTokenSelection());
|
|
35
|
+
|
|
36
|
+
describe("selection shapes", () => {
|
|
37
|
+
it("prefers the >=1.7 signed-cookie arm", () => {
|
|
38
|
+
expect(currentSelection()).toEqual({ useAccountCookie: true });
|
|
39
|
+
// Both 1.7 arms are strictObject, so a stray key is as fatal as a wrong
|
|
40
|
+
// one: assert the exact key set, not just the absence of providerId.
|
|
41
|
+
expect(Object.keys(currentSelection())).toEqual(["useAccountCookie"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("falls back to the <=1.6 provider-id body", () => {
|
|
45
|
+
expect(legacySelection("keycloak")).toEqual({ providerId: "keycloak" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("carries userId where a caller has no session to identify by", () => {
|
|
49
|
+
expect(currentSelection("u1")).toEqual({ useAccountCookie: true, userId: "u1" });
|
|
50
|
+
expect(legacySelection("keycloak", "u1")).toEqual({ providerId: "keycloak", userId: "u1" });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("treats only a 400 VALIDATION_ERROR as a wire-shape refusal", () => {
|
|
54
|
+
expect(isBodyRefusal(refusal)).toBe(true);
|
|
55
|
+
expect(isBodyRefusal(unauthorized)).toBe(false);
|
|
56
|
+
expect(isBodyRefusal({ status: 400, body: { code: "FAILED_TO_GET_ACCESS_TOKEN" } })).toBe(
|
|
57
|
+
false,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("requestAccessToken negotiation", () => {
|
|
63
|
+
it("sends one request when the server takes the current body", async () => {
|
|
64
|
+
const send = vi.fn().mockResolvedValue(ok);
|
|
65
|
+
await requestAccessToken("keycloak", send);
|
|
66
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
67
|
+
expect(send).toHaveBeenCalledWith({ useAccountCookie: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("retries with the legacy body when the current one is refused", async () => {
|
|
71
|
+
const send = vi.fn().mockResolvedValueOnce(refusal).mockResolvedValueOnce(ok);
|
|
72
|
+
const res = await requestAccessToken("keycloak", send);
|
|
73
|
+
expect(send).toHaveBeenNthCalledWith(1, { useAccountCookie: true });
|
|
74
|
+
expect(send).toHaveBeenNthCalledWith(2, { providerId: "keycloak" });
|
|
75
|
+
expect(res).toEqual(ok);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("remembers the legacy server and stops paying the extra round trip", async () => {
|
|
79
|
+
const send = vi.fn().mockResolvedValueOnce(refusal).mockResolvedValue(unauthorized);
|
|
80
|
+
await requestAccessToken("keycloak", send);
|
|
81
|
+
send.mockClear();
|
|
82
|
+
await requestAccessToken("keycloak", send);
|
|
83
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(send).toHaveBeenCalledWith({ providerId: "keycloak" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("does not retry a 401 — that is an answer about the session, not the body", async () => {
|
|
88
|
+
const send = vi.fn().mockResolvedValue(unauthorized);
|
|
89
|
+
await requestAccessToken("keycloak", send);
|
|
90
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns the second refusal rather than looping when neither shape lands", async () => {
|
|
94
|
+
const send = vi.fn().mockResolvedValue(refusal);
|
|
95
|
+
const res = await requestAccessToken("keycloak", send);
|
|
96
|
+
expect(send).toHaveBeenCalledTimes(2);
|
|
97
|
+
expect(res).toEqual(refusal);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("against the installed Better Auth router", () => {
|
|
102
|
+
const auth = createAuth({
|
|
103
|
+
baseUrl,
|
|
104
|
+
secret: "test-secret-not-a-real-one-0123456789",
|
|
105
|
+
expoScheme: "test",
|
|
106
|
+
keycloak: {
|
|
107
|
+
baseUrl: "https://auth.example.test",
|
|
108
|
+
realm: "test",
|
|
109
|
+
clientId: "test-client",
|
|
110
|
+
clientSecret: "",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("negotiates a body this server validates", async () => {
|
|
115
|
+
const res = await requestAccessToken("keycloak", async (body) => {
|
|
116
|
+
const response = await auth.handler(
|
|
117
|
+
new Request(`${baseUrl}/api/auth/get-access-token`, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "Content-Type": "application/json", Origin: baseUrl },
|
|
120
|
+
body: JSON.stringify(body),
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
return {
|
|
124
|
+
status: response.status,
|
|
125
|
+
body: (await response.json().catch(() => null)) as { code?: string } | null,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
// 401: the body got past validation and the endpoint asked for a session.
|
|
129
|
+
expect(res.status).toBe(401);
|
|
130
|
+
});
|
|
131
|
+
});
|