@multiplatform.one/keycloak 7.6.3 → 7.7.1
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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How we ask Better Auth's `/get-access-token` for a provider access token.
|
|
3
|
+
*
|
|
4
|
+
* Better Auth changed this body between minors and the two shapes are
|
|
5
|
+
* mutually exclusive:
|
|
6
|
+
*
|
|
7
|
+
* <=1.6 body: z.object({ providerId, accountId?, userId? })
|
|
8
|
+
* >=1.7 body: z.union([ z.strictObject({ accountId, userId? }),
|
|
9
|
+
* z.strictObject({ useAccountCookie: true, userId? }) ])
|
|
10
|
+
*
|
|
11
|
+
* 1.7 dropped `providerId` (it survives only as a response field) and made
|
|
12
|
+
* both arms STRICT, so a 1.6 body fails 1.7 outright; and 1.6 requires
|
|
13
|
+
* `providerId`, so a 1.7 body fails 1.6. No single body satisfies both.
|
|
14
|
+
*
|
|
15
|
+
* We are a library. The server version is whatever the consuming app's
|
|
16
|
+
* lockfile resolved, and we do not get to know it — which is exactly how this
|
|
17
|
+
* broke in the first place: the client kept sending the 1.6 body while an
|
|
18
|
+
* app's `^1.6.9` floated its deployed server to 1.7, every token fetch 400'd
|
|
19
|
+
* at body validation, and the Frappe layer fell back to guest cookies while
|
|
20
|
+
* the UI still said the user was signed in.
|
|
21
|
+
*
|
|
22
|
+
* So negotiate instead of guessing: send the current shape, and on a body
|
|
23
|
+
* validation refusal send the legacy one, remembering the answer for the rest
|
|
24
|
+
* of the process. At most one extra request per process, and an app that
|
|
25
|
+
* upgrades or downgrades Better Auth keeps working with no code change.
|
|
26
|
+
*
|
|
27
|
+
* The cookie arm is the only 1.7 arm we can use: `createAuth` runs stateless
|
|
28
|
+
* with `account.storeAccountCookie`, so there is no account row to address by
|
|
29
|
+
* id.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export type AccessTokenSelection =
|
|
33
|
+
| { useAccountCookie: true; userId?: string }
|
|
34
|
+
| { providerId: string; userId?: string };
|
|
35
|
+
|
|
36
|
+
export type AccessTokenResponse = {
|
|
37
|
+
status: number;
|
|
38
|
+
body: { code?: string } | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Better Auth's code for a body that failed the endpoint's schema. */
|
|
42
|
+
const validationErrorCode = "VALIDATION_ERROR";
|
|
43
|
+
|
|
44
|
+
/** Which body shape this server accepted, once we know. */
|
|
45
|
+
let known: "current" | "legacy" | undefined;
|
|
46
|
+
|
|
47
|
+
/** Forget the negotiated shape (tests, and a server swapped under a client). */
|
|
48
|
+
export function resetAccessTokenSelection() {
|
|
49
|
+
known = undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The >=1.7 body: the account comes from its signed cookie. */
|
|
53
|
+
export function currentSelection(userId?: string): AccessTokenSelection {
|
|
54
|
+
return userId ? { useAccountCookie: true, userId } : { useAccountCookie: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The <=1.6 body: the account is chosen by provider id. */
|
|
58
|
+
export function legacySelection(providerId: string, userId?: string): AccessTokenSelection {
|
|
59
|
+
return userId ? { providerId, userId } : { providerId };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* True for "this server does not understand that body" — the only answer
|
|
64
|
+
* worth retrying with the other shape. A 401, a 403 or a dead refresh token
|
|
65
|
+
* are real answers about the session, not about the wire format.
|
|
66
|
+
*/
|
|
67
|
+
export function isBodyRefusal(res: AccessTokenResponse): boolean {
|
|
68
|
+
return res.status === 400 && res.body?.code === validationErrorCode;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Ask for an access token, negotiating the body shape.
|
|
73
|
+
*
|
|
74
|
+
* `send` posts one body and reports the status plus the parsed JSON (null
|
|
75
|
+
* when there is no JSON to parse). It is called at most twice, and only twice
|
|
76
|
+
* on the first call of a process against a server whose shape we have not
|
|
77
|
+
* learned yet.
|
|
78
|
+
*/
|
|
79
|
+
export async function requestAccessToken(
|
|
80
|
+
providerId: string,
|
|
81
|
+
send: (body: AccessTokenSelection) => Promise<AccessTokenResponse>,
|
|
82
|
+
userId?: string,
|
|
83
|
+
): Promise<AccessTokenResponse> {
|
|
84
|
+
const current = () => currentSelection(userId);
|
|
85
|
+
const legacy = () => legacySelection(providerId, userId);
|
|
86
|
+
|
|
87
|
+
if (known === "legacy") return send(legacy());
|
|
88
|
+
if (known === "current") return send(current());
|
|
89
|
+
|
|
90
|
+
const first = await send(current());
|
|
91
|
+
if (!isBodyRefusal(first)) {
|
|
92
|
+
if (first.status !== 400) known = "current";
|
|
93
|
+
return first;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const second = await send(legacy());
|
|
97
|
+
if (!isBodyRefusal(second)) known = "legacy";
|
|
98
|
+
return second;
|
|
99
|
+
}
|
|
@@ -16,6 +16,7 @@ import { Platform } from "react-native";
|
|
|
16
16
|
import * as Linking from "expo-linking";
|
|
17
17
|
import * as SecureStore from "expo-secure-store";
|
|
18
18
|
import * as WebBrowser from "expo-web-browser";
|
|
19
|
+
import { requestAccessToken } from "./accountSelection";
|
|
19
20
|
|
|
20
21
|
try {
|
|
21
22
|
WebBrowser.maybeCompleteAuthSession?.();
|
|
@@ -104,7 +105,13 @@ export interface ExpoAuthFlow {
|
|
|
104
105
|
*/
|
|
105
106
|
signOut(postLogoutRedirectUri?: string): Promise<void>;
|
|
106
107
|
getSession(): Promise<{ user?: { id: string; email?: string; name?: string } } | null>;
|
|
107
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Valid provider access token (server refreshes when expired).
|
|
110
|
+
*
|
|
111
|
+
* `providerId` is kept for call-site readability only. The server picks the
|
|
112
|
+
* account from its signed account cookie, which is the sole selector a
|
|
113
|
+
* stateless Better Auth deployment has — see `accountSelection.ts`.
|
|
114
|
+
*/
|
|
108
115
|
getAccessToken(
|
|
109
116
|
providerId: string,
|
|
110
117
|
): Promise<{ accessToken?: string; accessTokenExpiresAt?: string } | null>;
|
|
@@ -356,12 +363,17 @@ export function createExpoAuthFlow(baseURL: string, scheme?: string): ExpoAuthFl
|
|
|
356
363
|
},
|
|
357
364
|
|
|
358
365
|
async getAccessToken(providerId: string) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
366
|
+
let payload: unknown = null;
|
|
367
|
+
const res = await requestAccessToken(providerId, async (body) => {
|
|
368
|
+
const response = await authFetch("/get-access-token", {
|
|
369
|
+
method: "POST",
|
|
370
|
+
body: JSON.stringify(body),
|
|
371
|
+
});
|
|
372
|
+
payload = await response.json().catch(() => null);
|
|
373
|
+
return { status: response.status, body: payload as { code?: string } | null };
|
|
362
374
|
});
|
|
363
|
-
if (
|
|
364
|
-
return
|
|
375
|
+
if (res.status < 200 || res.status >= 300) return null;
|
|
376
|
+
return payload as {
|
|
365
377
|
accessToken?: string;
|
|
366
378
|
accessTokenExpiresAt?: string;
|
|
367
379
|
} | null;
|
package/src/frappeToken.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* backoff, keep the session. A Keycloak restart must not look like logout.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { requestAccessToken } from "./betterAuth/accountSelection";
|
|
22
23
|
import { getAuthClient } from "./betterAuth/client";
|
|
23
24
|
|
|
24
25
|
let cached: { token: string; expiresAt: number } | undefined;
|
|
@@ -92,17 +93,21 @@ async function fetchTokenOnce(): Promise<
|
|
|
92
93
|
{ ok: true; token: string; expiresAt: number } | { ok: false; kind: FailureKind }
|
|
93
94
|
> {
|
|
94
95
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
let payload: unknown = null;
|
|
97
|
+
const res = await requestAccessToken("keycloak", async (selection) => {
|
|
98
|
+
const response = await fetch(`${window.location.origin}/api/auth/get-access-token`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
credentials: "include",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify(selection),
|
|
103
|
+
});
|
|
104
|
+
payload = await response.json().catch(() => null);
|
|
105
|
+
return { status: response.status, body: payload as { code?: string } | null };
|
|
100
106
|
});
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
return { ok: false, kind: classifyAccessTokenResponse(res.status, body) };
|
|
107
|
+
if (res.status < 200 || res.status >= 300) {
|
|
108
|
+
return { ok: false, kind: classifyAccessTokenResponse(res.status, res.body) };
|
|
104
109
|
}
|
|
105
|
-
const data =
|
|
110
|
+
const data = payload as {
|
|
106
111
|
accessToken?: string;
|
|
107
112
|
accessTokenExpiresAt?: string;
|
|
108
113
|
} | null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How we ask Better Auth's `/get-access-token` for a provider access token.
|
|
3
|
+
*
|
|
4
|
+
* Better Auth changed this body between minors and the two shapes are
|
|
5
|
+
* mutually exclusive:
|
|
6
|
+
*
|
|
7
|
+
* <=1.6 body: z.object({ providerId, accountId?, userId? })
|
|
8
|
+
* >=1.7 body: z.union([ z.strictObject({ accountId, userId? }),
|
|
9
|
+
* z.strictObject({ useAccountCookie: true, userId? }) ])
|
|
10
|
+
*
|
|
11
|
+
* 1.7 dropped `providerId` (it survives only as a response field) and made
|
|
12
|
+
* both arms STRICT, so a 1.6 body fails 1.7 outright; and 1.6 requires
|
|
13
|
+
* `providerId`, so a 1.7 body fails 1.6. No single body satisfies both.
|
|
14
|
+
*
|
|
15
|
+
* We are a library. The server version is whatever the consuming app's
|
|
16
|
+
* lockfile resolved, and we do not get to know it — which is exactly how this
|
|
17
|
+
* broke in the first place: the client kept sending the 1.6 body while an
|
|
18
|
+
* app's `^1.6.9` floated its deployed server to 1.7, every token fetch 400'd
|
|
19
|
+
* at body validation, and the Frappe layer fell back to guest cookies while
|
|
20
|
+
* the UI still said the user was signed in.
|
|
21
|
+
*
|
|
22
|
+
* So negotiate instead of guessing: send the current shape, and on a body
|
|
23
|
+
* validation refusal send the legacy one, remembering the answer for the rest
|
|
24
|
+
* of the process. At most one extra request per process, and an app that
|
|
25
|
+
* upgrades or downgrades Better Auth keeps working with no code change.
|
|
26
|
+
*
|
|
27
|
+
* The cookie arm is the only 1.7 arm we can use: `createAuth` runs stateless
|
|
28
|
+
* with `account.storeAccountCookie`, so there is no account row to address by
|
|
29
|
+
* id.
|
|
30
|
+
*/
|
|
31
|
+
export type AccessTokenSelection = {
|
|
32
|
+
useAccountCookie: true;
|
|
33
|
+
userId?: string;
|
|
34
|
+
} | {
|
|
35
|
+
providerId: string;
|
|
36
|
+
userId?: string;
|
|
37
|
+
};
|
|
38
|
+
export type AccessTokenResponse = {
|
|
39
|
+
status: number;
|
|
40
|
+
body: {
|
|
41
|
+
code?: string;
|
|
42
|
+
} | null;
|
|
43
|
+
};
|
|
44
|
+
/** Forget the negotiated shape (tests, and a server swapped under a client). */
|
|
45
|
+
export declare function resetAccessTokenSelection(): void;
|
|
46
|
+
/** The >=1.7 body: the account comes from its signed cookie. */
|
|
47
|
+
export declare function currentSelection(userId?: string): AccessTokenSelection;
|
|
48
|
+
/** The <=1.6 body: the account is chosen by provider id. */
|
|
49
|
+
export declare function legacySelection(providerId: string, userId?: string): AccessTokenSelection;
|
|
50
|
+
/**
|
|
51
|
+
* True for "this server does not understand that body" — the only answer
|
|
52
|
+
* worth retrying with the other shape. A 401, a 403 or a dead refresh token
|
|
53
|
+
* are real answers about the session, not about the wire format.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isBodyRefusal(res: AccessTokenResponse): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Ask for an access token, negotiating the body shape.
|
|
58
|
+
*
|
|
59
|
+
* `send` posts one body and reports the status plus the parsed JSON (null
|
|
60
|
+
* when there is no JSON to parse). It is called at most twice, and only twice
|
|
61
|
+
* on the first call of a process against a server whose shape we have not
|
|
62
|
+
* learned yet.
|
|
63
|
+
*/
|
|
64
|
+
export declare function requestAccessToken(providerId: string, send: (body: AccessTokenSelection) => Promise<AccessTokenResponse>, userId?: string): Promise<AccessTokenResponse>;
|
|
65
|
+
//# sourceMappingURL=accountSelection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accountSelection.d.ts","sourceRoot":"","sources":["../../src/betterAuth/accountSelection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,gBAAgB,EAAE,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAChC,CAAC;AAQF,gFAAgF;AAChF,wBAAgB,yBAAyB,SAExC;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,oBAAoB,CAEtE;AAED,4DAA4D;AAC5D,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,oBAAoB,CAEzF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAE/D;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,OAAO,CAAC,mBAAmB,CAAC,EAClE,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,CAAC,CAgB9B"}
|
|
@@ -28,7 +28,13 @@ export interface ExpoAuthFlow {
|
|
|
28
28
|
name?: string;
|
|
29
29
|
};
|
|
30
30
|
} | null>;
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Valid provider access token (server refreshes when expired).
|
|
33
|
+
*
|
|
34
|
+
* `providerId` is kept for call-site readability only. The server picks the
|
|
35
|
+
* account from its signed account cookie, which is the sole selector a
|
|
36
|
+
* stateless Better Auth deployment has — see `accountSelection.ts`.
|
|
37
|
+
*/
|
|
32
38
|
getAccessToken(providerId: string): Promise<{
|
|
33
39
|
accessToken?: string;
|
|
34
40
|
accessTokenExpiresAt?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"expoAuthFlow.native.d.ts","sourceRoot":"","sources":["../../src/betterAuth/expoAuthFlow.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"expoAuthFlow.native.d.ts","sourceRoot":"","sources":["../../src/betterAuth/expoAuthFlow.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoFH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,UAAU,IAAI,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACvF;;;;;;OAMG;IACH,cAAc,CACZ,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;CAC5E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGvE;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsB3E;AAqED,wBAAgB,qBAAqB,IAAI,YAAY,GAAG,IAAI,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAqHjF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frappeToken.d.ts","sourceRoot":"","sources":["../src/frappeToken.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;
|
|
1
|
+
{"version":3,"file":"frappeToken.d.ts","sourceRoot":"","sources":["../src/frappeToken.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAiDH;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAW1E;AAgED,wDAAwD;AACxD,wBAAgB,wBAAwB,SAKvC;AAED;;;;GAIG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,IAAI,CAiB/D"}
|