@reatomized/convex 0.2.0 → 0.3.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/README.md
CHANGED
|
@@ -83,6 +83,8 @@ await convexAuth.signOut();
|
|
|
83
83
|
whose own client is a React provider. It uses the same storage keys, so sessions
|
|
84
84
|
carry over from `@convex-dev/auth/react`. On start it signs in with a `code`
|
|
85
85
|
from the URL, and it refreshes the token once across tabs.
|
|
86
|
+
`completeSignIn(code)` runs that exchange for an app that reads the URL itself;
|
|
87
|
+
its `error` atom holds a failed exchange either way.
|
|
86
88
|
|
|
87
89
|
```ts
|
|
88
90
|
// Better Auth with @convex-dev/better-auth
|
|
@@ -104,7 +106,9 @@ await authClient.signIn.email({ email, password });
|
|
|
104
106
|
Better Auth's client works without a framework, so you sign in through it;
|
|
105
107
|
`reatomBetterAuthProvider` follows its session store, fetches Convex tokens, and
|
|
106
108
|
trades the `ott` one-time token from a cross-domain OAuth redirect for the
|
|
107
|
-
session.
|
|
109
|
+
session. Its `data` atom is the store's session and user, typed by your client,
|
|
110
|
+
and `completeSignIn(ott)` is the one-time token trade with an `error` atom.
|
|
111
|
+
Neither entry point adds dependencies.
|
|
108
112
|
|
|
109
113
|
For another library, pass any object with `isLoading` and `session` atoms and a
|
|
110
114
|
`fetchToken` function. `session` should change when the identity does, such as a
|
|
@@ -4,17 +4,18 @@
|
|
|
4
4
|
* `ConvexBetterAuthProvider`. Sign in and out through the better-auth client
|
|
5
5
|
* itself; its session store tells this provider.
|
|
6
6
|
*/
|
|
7
|
-
import { type AtomLike } from "@reatom/core";
|
|
7
|
+
import { type Action, type AsyncExt, type AtomLike, type Computed } from "@reatom/core";
|
|
8
8
|
import type { AuthProvider } from "./auth_state.js";
|
|
9
9
|
export type { AuthProvider } from "./auth_state.js";
|
|
10
10
|
type SessionState = {
|
|
11
11
|
data: {
|
|
12
12
|
session?: {
|
|
13
13
|
id: string;
|
|
14
|
-
} | null;
|
|
14
|
+
} | null | undefined;
|
|
15
15
|
} | null;
|
|
16
16
|
isPending: boolean;
|
|
17
17
|
};
|
|
18
|
+
type SessionData<Client extends BetterAuthClient> = ReturnType<Client["useSession"]["get"]>["data"];
|
|
18
19
|
/**
|
|
19
20
|
* What the provider uses of a client from better-auth's `createAuthClient`
|
|
20
21
|
* (`better-auth/client`) with `@convex-dev/better-auth`'s `convexClient()`
|
|
@@ -35,8 +36,8 @@ export type BetterAuthClient = {
|
|
|
35
36
|
};
|
|
36
37
|
}): Promise<{
|
|
37
38
|
data?: {
|
|
38
|
-
token?: string | null;
|
|
39
|
-
} | null;
|
|
39
|
+
token?: string | null | undefined;
|
|
40
|
+
} | null | undefined;
|
|
40
41
|
}>;
|
|
41
42
|
};
|
|
42
43
|
getSession(options?: {
|
|
@@ -52,8 +53,11 @@ export type BetterAuthClient = {
|
|
|
52
53
|
data?: {
|
|
53
54
|
session?: {
|
|
54
55
|
token: string;
|
|
55
|
-
} | null;
|
|
56
|
-
} | null;
|
|
56
|
+
} | null | undefined;
|
|
57
|
+
} | null | undefined;
|
|
58
|
+
error?: {
|
|
59
|
+
message?: string | undefined;
|
|
60
|
+
} | null | undefined;
|
|
57
61
|
}>;
|
|
58
62
|
};
|
|
59
63
|
};
|
|
@@ -63,13 +67,26 @@ export type BetterAuthClient = {
|
|
|
63
67
|
export type BetterAuthOptions = {
|
|
64
68
|
/** A Convex token from server rendering, used until the session loads. */
|
|
65
69
|
initialToken?: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Whether an `ott` query param on start is a one-time token to sign in with;
|
|
72
|
+
* `true` by default. Only `crossDomainClient()` sends one.
|
|
73
|
+
*/
|
|
74
|
+
shouldHandleOneTimeToken?: boolean;
|
|
66
75
|
/** Drops `ott` from the address; `history.replaceState` by default. */
|
|
67
76
|
replaceURL?: (url: URL) => void | Promise<void>;
|
|
68
77
|
};
|
|
69
78
|
/** @public */
|
|
70
|
-
export type ReatomBetterAuth = AuthProvider & {
|
|
79
|
+
export type ReatomBetterAuth<Data = SessionState["data"]> = AuthProvider & {
|
|
80
|
+
/** The session store's data: the session and user, `null` when signed out. */
|
|
81
|
+
readonly data: Computed<Data>;
|
|
71
82
|
/** The last Convex token, `null` without one. */
|
|
72
83
|
readonly token: AtomLike<string | null, []>;
|
|
84
|
+
/**
|
|
85
|
+
* Trades the one-time token a cross-domain OAuth redirect comes back with
|
|
86
|
+
* for the session. Runs on start for an `ott` in the URL; call it when the
|
|
87
|
+
* app takes the token from the URL itself. Its `error` holds a failed trade.
|
|
88
|
+
*/
|
|
89
|
+
readonly completeSignIn: Action<[oneTimeToken: string], Promise<void>> & AsyncExt<[oneTimeToken: string], void>;
|
|
73
90
|
};
|
|
74
91
|
/**
|
|
75
92
|
* With `crossDomainClient()`, an OAuth sign-in comes back with a one-time
|
|
@@ -86,4 +103,4 @@ export type ReatomBetterAuth = AuthProvider & {
|
|
|
86
103
|
*
|
|
87
104
|
* @public
|
|
88
105
|
*/
|
|
89
|
-
export declare function reatomBetterAuthProvider(authClient:
|
|
106
|
+
export declare function reatomBetterAuthProvider<Client extends BetterAuthClient>(authClient: Client, options?: BetterAuthOptions, name?: string): ReatomBetterAuth<SessionData<Client>>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `ConvexBetterAuthProvider`. Sign in and out through the better-auth client
|
|
5
5
|
* itself; its session store tells this provider.
|
|
6
6
|
*/
|
|
7
|
-
import { action, atom, computed, peek, withConnectHook, wrap, } from "@reatom/core";
|
|
7
|
+
import { action, atom, computed, peek, withAsync, withConnectHook, wrap, } from "@reatom/core";
|
|
8
8
|
/**
|
|
9
9
|
* With `crossDomainClient()`, an OAuth sign-in comes back with a one-time
|
|
10
10
|
* token in the `ott` query param; the provider trades it for the session.
|
|
@@ -25,15 +25,37 @@ export function reatomBetterAuthProvider(authClient, options = {}, name = "bette
|
|
|
25
25
|
// Boxed, so the atom holds the request rather than its result.
|
|
26
26
|
const pending = atom(null, `${name}._pending`);
|
|
27
27
|
const state = atom(() => authClient.useSession.get(), `${name}.sessionState`).extend(withConnectHook((target) => {
|
|
28
|
-
void
|
|
28
|
+
void start();
|
|
29
29
|
return authClient.useSession.subscribe(wrap((value) => {
|
|
30
|
+
const previous = peek(target).data?.session?.id;
|
|
30
31
|
target.set(value);
|
|
31
|
-
if (
|
|
32
|
+
if (value.isPending)
|
|
33
|
+
return;
|
|
34
|
+
// The cached token belongs to the previous session, or to none.
|
|
35
|
+
const id = value.data?.session?.id;
|
|
36
|
+
if (!id || (previous && id !== previous))
|
|
32
37
|
token.set(null);
|
|
33
38
|
}));
|
|
34
39
|
}));
|
|
35
|
-
const
|
|
36
|
-
|
|
40
|
+
const completeSignIn = action(async (oneTimeToken) => {
|
|
41
|
+
// better-auth's client answers every property, so a missing plugin shows
|
|
42
|
+
// up as a failed request rather than an undefined `crossDomain`.
|
|
43
|
+
const result = await wrap(authClient.crossDomain.oneTimeToken.verify({ token: oneTimeToken }));
|
|
44
|
+
const session = result.data?.session;
|
|
45
|
+
if (!session)
|
|
46
|
+
throw new Error(result.error?.message ?? "Invalid one-time token");
|
|
47
|
+
await wrap(authClient.getSession({
|
|
48
|
+
fetchOptions: { headers: { Authorization: `Bearer ${session.token}` } },
|
|
49
|
+
}));
|
|
50
|
+
authClient.updateSession?.();
|
|
51
|
+
}, `${name}.completeSignIn`).extend(withAsync());
|
|
52
|
+
const started = atom(false, `${name}._started`);
|
|
53
|
+
const start = action(async () => {
|
|
54
|
+
if (peek(started))
|
|
55
|
+
return;
|
|
56
|
+
started.set(true);
|
|
57
|
+
if (options.shouldHandleOneTimeToken === false ||
|
|
58
|
+
typeof location === "undefined")
|
|
37
59
|
return;
|
|
38
60
|
const url = new URL(location.href);
|
|
39
61
|
const ott = url.searchParams.get("ott");
|
|
@@ -42,15 +64,13 @@ export function reatomBetterAuthProvider(authClient, options = {}, name = "bette
|
|
|
42
64
|
url.searchParams.delete("ott");
|
|
43
65
|
await wrap((options.replaceURL ??
|
|
44
66
|
((next) => history.replaceState({}, "", next)))(url));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
authClient.updateSession?.();
|
|
53
|
-
}, `${name}._verifyOneTimeToken`);
|
|
67
|
+
try {
|
|
68
|
+
await wrap(completeSignIn(ott));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Kept in `completeSignIn.error()`.
|
|
72
|
+
}
|
|
73
|
+
}, `${name}._start`);
|
|
54
74
|
const fetchToken = action(async ({ forceRefreshToken }) => {
|
|
55
75
|
const cached = token();
|
|
56
76
|
if (cached && !forceRefreshToken)
|
|
@@ -74,7 +94,8 @@ export function reatomBetterAuthProvider(authClient, options = {}, name = "bette
|
|
|
74
94
|
}
|
|
75
95
|
}, `${name}.fetchToken`);
|
|
76
96
|
return {
|
|
77
|
-
isLoading: computed(() => state().isPending && token() === null, `${name}.isLoading`),
|
|
97
|
+
isLoading: computed(() => (state().isPending && token() === null) || !completeSignIn.ready(), `${name}.isLoading`),
|
|
98
|
+
data: computed(() => state().data, `${name}.data`),
|
|
78
99
|
// A token from server rendering stands in until the session loads.
|
|
79
100
|
session: computed(() => {
|
|
80
101
|
const id = state().data?.session?.id;
|
|
@@ -84,5 +105,6 @@ export function reatomBetterAuthProvider(authClient, options = {}, name = "bette
|
|
|
84
105
|
}, `${name}.session`),
|
|
85
106
|
fetchToken,
|
|
86
107
|
token,
|
|
108
|
+
completeSignIn,
|
|
87
109
|
};
|
|
88
110
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* verifier, signs in. `auth:signOut` ends the server session. Storage keys and
|
|
9
9
|
* formats are `@convex-dev/auth/react`'s, so its sessions carry over.
|
|
10
10
|
*/
|
|
11
|
-
import { type Action, type AtomLike } from "@reatom/core";
|
|
11
|
+
import { type Action, type AsyncExt, type AtomLike } from "@reatom/core";
|
|
12
12
|
import type { Value } from "convex/values";
|
|
13
13
|
import type { AuthProvider } from "./auth_state.js";
|
|
14
14
|
import type { ReatomConvexClient } from "./client.js";
|
|
@@ -62,6 +62,13 @@ export type ReatomConvexAuth = AuthProvider & {
|
|
|
62
62
|
provider?: string | undefined,
|
|
63
63
|
params?: FormData | Record<string, Value> | undefined
|
|
64
64
|
], Promise<ConvexAuthSignInResult>>;
|
|
65
|
+
/**
|
|
66
|
+
* Signs in with the `code` the backend sends the browser back with after
|
|
67
|
+
* OAuth; `true` when tokens were issued. Runs on start for a `code` in the
|
|
68
|
+
* URL; call it when the app takes the code from the URL itself. Its `error`
|
|
69
|
+
* holds a failed exchange.
|
|
70
|
+
*/
|
|
71
|
+
readonly completeSignIn: Action<[code: string], Promise<boolean>> & AsyncExt<[code: string], boolean>;
|
|
65
72
|
/** Ends the session locally, even when `auth:signOut` fails. */
|
|
66
73
|
readonly signOut: Action<[], Promise<void>>;
|
|
67
74
|
};
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* verifier, signs in. `auth:signOut` ends the server session. Storage keys and
|
|
9
9
|
* formats are `@convex-dev/auth/react`'s, so its sessions carry over.
|
|
10
10
|
*/
|
|
11
|
-
import { action, atom, computed, peek, withConnectHook, wrap, } from "@reatom/core";
|
|
11
|
+
import { action, atom, computed, peek, withAsync, withConnectHook, wrap, } from "@reatom/core";
|
|
12
12
|
import { ConvexHttpClient } from "convex/browser";
|
|
13
13
|
import { makeFunctionReference } from "convex/server";
|
|
14
14
|
import { jwtDecode } from "../../vendor/jwt-decode/index.js";
|
|
@@ -56,7 +56,7 @@ export function reatomConvexAuthProvider(convex, options = {}, name = `${convex.
|
|
|
56
56
|
stopWarning?.();
|
|
57
57
|
};
|
|
58
58
|
}));
|
|
59
|
-
const
|
|
59
|
+
const restoring = atom(true, `${name}._restoring`).extend(withConnectHook(() => {
|
|
60
60
|
void start();
|
|
61
61
|
}));
|
|
62
62
|
const isRefreshingToken = atom(false, `${name}.isRefreshingToken`);
|
|
@@ -76,7 +76,7 @@ export function reatomConvexAuthProvider(convex, options = {}, name = `${convex.
|
|
|
76
76
|
}, `${name}.session`);
|
|
77
77
|
const apply = (value) => {
|
|
78
78
|
token.set(value);
|
|
79
|
-
|
|
79
|
+
restoring.set(false);
|
|
80
80
|
};
|
|
81
81
|
const store = async (tokens) => {
|
|
82
82
|
await write(JWT_KEY, tokens?.token ?? null);
|
|
@@ -121,6 +121,11 @@ export function reatomConvexAuthProvider(convex, options = {}, name = `${convex.
|
|
|
121
121
|
}
|
|
122
122
|
return { signingIn: false };
|
|
123
123
|
}, `${name}.signIn`);
|
|
124
|
+
const completeSignIn = action(async (code) => {
|
|
125
|
+
const { signingIn } = await wrap(signIn(undefined, { code }));
|
|
126
|
+
return signingIn;
|
|
127
|
+
}, `${name}.completeSignIn`).extend(withAsync());
|
|
128
|
+
const isLoading = computed(() => restoring() || !completeSignIn.ready(), `${name}.isLoading`);
|
|
124
129
|
const signOut = action(async () => {
|
|
125
130
|
try {
|
|
126
131
|
await wrap(convex.action(signOutRef, {}));
|
|
@@ -167,10 +172,13 @@ export function reatomConvexAuthProvider(convex, options = {}, name = `${convex.
|
|
|
167
172
|
await wrap((options.replaceURL ??
|
|
168
173
|
((relative) => history.replaceState({}, "", relative)))(url.pathname + url.search + url.hash));
|
|
169
174
|
try {
|
|
170
|
-
await wrap(
|
|
175
|
+
await wrap(completeSignIn(code));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Kept in `completeSignIn.error()`.
|
|
171
179
|
}
|
|
172
180
|
finally {
|
|
173
|
-
|
|
181
|
+
restoring.set(false);
|
|
174
182
|
}
|
|
175
183
|
return;
|
|
176
184
|
}
|
|
@@ -209,6 +217,7 @@ export function reatomConvexAuthProvider(convex, options = {}, name = `${convex.
|
|
|
209
217
|
token,
|
|
210
218
|
isRefreshingToken,
|
|
211
219
|
signIn,
|
|
220
|
+
completeSignIn,
|
|
212
221
|
signOut,
|
|
213
222
|
};
|
|
214
223
|
}
|