@meshagent/meshagent-react-auth 0.36.3 → 0.37.2

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.
@@ -1,227 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildOAuthAuthorizeUrl = buildOAuthAuthorizeUrl;
4
3
  exports.useLoginScope = useLoginScope;
5
- const react_1 = require("react");
6
- const react_query_1 = require("@tanstack/react-query");
7
- const meshagent_ts_auth_1 = require("@meshagent/meshagent-ts-auth");
8
- const use_meshagent_auth_1 = require("./use-meshagent-auth");
9
- function toUrl(value) {
10
- return value instanceof URL ? value : new URL(value);
11
- }
12
- class UserProfileFetchError extends Error {
13
- constructor(status, message) {
14
- super(message);
15
- Object.defineProperty(this, "status", {
16
- enumerable: true,
17
- configurable: true,
18
- writable: true,
19
- value: void 0
20
- });
21
- this.name = "UserProfileFetchError";
22
- this.status = status;
23
- }
24
- }
25
- function isAuthProfileError(error) {
26
- return error instanceof UserProfileFetchError
27
- && (error.status === 401 || error.status === 403);
28
- }
29
- async function fetchUserProfile(params) {
30
- const response = await fetch(new URL("/accounts/profiles/me", toUrl(params.serverUrl)), {
31
- signal: params.signal,
32
- headers: {
33
- "Authorization": `Bearer ${params.accessToken}`,
34
- "Accept": "application/json",
35
- },
36
- });
37
- if (!response.ok) {
38
- throw new UserProfileFetchError(response.status, `Failed to fetch user profile: ${response.status} ${await response.text()}`);
39
- }
40
- return await response.json();
41
- }
42
- function serializeExtraQueryParams(extraQueryParams) {
43
- return JSON.stringify(Object.entries(extraQueryParams ?? {}).sort(([left], [right]) => left.localeCompare(right)));
44
- }
45
- async function buildOAuthAuthorizeUrl(params) {
46
- const serverUrl = toUrl(params.serverUrl);
47
- const callbackUrl = toUrl(params.callbackUrl);
48
- const generator = new meshagent_ts_auth_1.PkceGenerator();
49
- const pair = await generator.generate();
50
- const cache = new meshagent_ts_auth_1.LocalStoragePkceCache(params.storage ?? (0, meshagent_ts_auth_1.getDefaultStorage)());
51
- await cache.saveVerifier(pair.codeVerifier);
52
- const query = new URLSearchParams({
53
- ...(params.extraQueryParams ?? {}),
54
- scope: params.scope ?? "profile",
55
- client_id: params.oauthClientId,
56
- code_challenge: pair.codeChallenge,
57
- response_type: "code",
58
- redirect_uri: callbackUrl.toString(),
59
- prompt: "select_account",
60
- });
61
- if (params.provider) {
62
- query.set("provider", params.provider);
63
- }
64
- return new URL(`/oauth/authorize?${query.toString()}`, serverUrl);
65
- }
66
- async function launchSignIn(params) {
67
- const authorizeUrl = await buildOAuthAuthorizeUrl({
68
- serverUrl: params.serverUrl,
69
- callbackUrl: params.callbackUrl,
70
- oauthClientId: params.oauthClientId,
71
- scope: params.scope,
72
- provider: params.provider,
73
- extraQueryParams: params.extraQueryParams,
74
- storage: params.storage,
75
- });
76
- if (typeof window === "undefined") {
77
- throw new Error("LoginScope requires a browser environment");
78
- }
79
- window.location.assign(authorizeUrl.toString());
80
- }
81
- function useLoginScope(options) {
82
- const storage = options.storage ?? (0, meshagent_ts_auth_1.getDefaultStorage)();
83
- const auth = options.auth ?? meshagent_ts_auth_1.meshagentAuth;
84
- const authSnapshot = (0, use_meshagent_auth_1.useMeshagentAuth)(auth);
85
- const session = (0, react_1.useMemo)(() => new meshagent_ts_auth_1.OAuthSessionManager({
86
- serverUrl: options.serverUrl,
87
- clientId: options.oauthClientId,
88
- auth,
89
- }), [auth, options.oauthClientId, options.serverUrl]);
90
- const autoSignIn = options.autoSignIn ?? true;
91
- const extraQueryParamsKey = (0, react_1.useMemo)(() => serializeExtraQueryParams(options.extraQueryParams), [options.extraQueryParams]);
92
- const [signInFailed, setSignInFailed] = (0, react_1.useState)(null);
93
- const [isSigningIn, setIsSigningIn] = (0, react_1.useState)(false);
94
- const [isCancelled, setIsCancelled] = (0, react_1.useState)(false);
95
- const [refreshVersion, setRefreshVersion] = (0, react_1.useState)(0);
96
- const signIn = async (provider) => {
97
- setIsSigningIn(true);
98
- setSignInFailed(null);
99
- setIsCancelled(false);
100
- try {
101
- await launchSignIn({
102
- serverUrl: options.serverUrl,
103
- callbackUrl: options.callbackUrl,
104
- oauthClientId: options.oauthClientId,
105
- scope: options.scope,
106
- provider,
107
- extraQueryParams: options.extraQueryParams,
108
- storage,
109
- });
110
- }
111
- catch (error) {
112
- setSignInFailed(error);
113
- setIsSigningIn(false);
114
- }
115
- };
116
- const loginQuery = (0, react_query_1.useQuery)({
117
- queryKey: [
118
- "login-scope",
119
- toUrl(options.serverUrl).toString(),
120
- toUrl(options.callbackUrl).toString(),
121
- options.oauthClientId,
122
- options.scope ?? "profile",
123
- extraQueryParamsKey,
124
- autoSignIn,
125
- authSnapshot.accessToken,
126
- refreshVersion,
127
- ],
128
- retry: false,
129
- refetchOnReconnect: false,
130
- refetchOnWindowFocus: false,
131
- queryFn: async ({ signal }) => {
132
- if (!authSnapshot.accessToken) {
133
- setSignInFailed(null);
134
- if (autoSignIn) {
135
- setIsSigningIn(true);
136
- try {
137
- await launchSignIn({
138
- serverUrl: options.serverUrl,
139
- callbackUrl: options.callbackUrl,
140
- oauthClientId: options.oauthClientId,
141
- scope: options.scope,
142
- provider: null,
143
- extraQueryParams: options.extraQueryParams,
144
- storage,
145
- });
146
- return {
147
- failed: null,
148
- isLoginLaunched: false,
149
- };
150
- }
151
- catch (error) {
152
- setIsSigningIn(false);
153
- return {
154
- failed: error,
155
- isLoginLaunched: true,
156
- };
157
- }
158
- }
159
- setIsSigningIn(false);
160
- return {
161
- failed: null,
162
- isLoginLaunched: true,
163
- };
164
- }
165
- let accessToken;
166
- try {
167
- accessToken = await session.getValidAccessTokenOrThrow();
168
- }
169
- catch (error) {
170
- auth.signOut();
171
- setIsSigningIn(false);
172
- return {
173
- failed: error,
174
- isLoginLaunched: true,
175
- };
176
- }
177
- try {
178
- const user = await fetchUserProfile({
179
- serverUrl: options.serverUrl,
180
- accessToken,
181
- signal,
182
- });
183
- auth.setUser(user);
184
- setSignInFailed(null);
185
- setIsSigningIn(false);
186
- return {
187
- failed: null,
188
- isLoginLaunched: false,
189
- };
190
- }
191
- catch (error) {
192
- setIsSigningIn(false);
193
- if (isAuthProfileError(error)) {
194
- auth.signOut();
195
- return {
196
- failed: error,
197
- isLoginLaunched: true,
198
- };
199
- }
200
- return {
201
- failed: error,
202
- isLoginLaunched: false,
203
- };
204
- }
205
- },
4
+ const use_auth_1 = require("./use-auth");
5
+ function useLoginScope(props) {
6
+ return (0, use_auth_1.useAuth)({
7
+ ...props,
8
+ scope: props.scope ?? "email",
206
9
  });
207
- const refresh = () => {
208
- setSignInFailed(null);
209
- setIsCancelled(false);
210
- setRefreshVersion((version) => version + 1);
211
- };
212
- const failed = signInFailed ?? (loginQuery.isFetching ? null : loginQuery.data?.failed ?? null);
213
- const isLoginLaunched = signInFailed !== null
214
- ? true
215
- : loginQuery.data?.isLoginLaunched ?? false;
216
- const refreshing = loginQuery.isPending || loginQuery.isFetching;
217
- return {
218
- failed,
219
- isCancelled,
220
- isSigningIn,
221
- refreshing,
222
- user: authSnapshot.user,
223
- signIn,
224
- refresh,
225
- isLoginLaunched,
226
- };
227
10
  }
@@ -1,4 +1,5 @@
1
- export { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
1
  export * from "./mauth-response";
2
+ export * from "./use-auth";
3
+ export * from "./use-ensure-login";
3
4
  export * from "./use-login-scope";
4
5
  export * from "./use-meshagent-auth";
package/dist/esm/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
1
  export * from "./mauth-response";
2
+ export * from "./use-auth";
3
+ export * from "./use-ensure-login";
3
4
  export * from "./use-login-scope";
4
5
  export * from "./use-meshagent-auth";
@@ -1,10 +1,18 @@
1
- import type { MAuthResponseProps } from "@meshagent/meshagent-ts-auth";
1
+ import type { StorageLike, MeshagentAuth } from "@meshagent/meshagent-ts-auth";
2
2
  declare enum UseMAuthResponseStatus {
3
3
  Idle = "idle",
4
4
  Loading = "loading",
5
5
  Success = "success",
6
6
  Error = "error"
7
7
  }
8
+ export interface MAuthResponseProps {
9
+ serverUrl?: string | URL;
10
+ callbackUrl: string | URL;
11
+ oauthClientId: string;
12
+ authorizationCode?: string;
13
+ storage?: StorageLike;
14
+ auth?: MeshagentAuth;
15
+ }
8
16
  export interface MAuthResponse {
9
17
  status: UseMAuthResponseStatus;
10
18
  error: string | null;
@@ -1,8 +1,10 @@
1
- import { useQuery } from "@tanstack/react-query";
1
+ import { useEffect, useState } from "react";
2
2
  import { getAuthorizationCode, exchangeToken } from "@meshagent/meshagent-ts-auth";
3
+ import { getReferenceKey } from "./reference-key";
3
4
  function toUrl(value) {
4
5
  return value instanceof URL ? value : new URL(value);
5
6
  }
7
+ const inFlightTokenExchanges = new Map();
6
8
  var UseMAuthResponseStatus;
7
9
  (function (UseMAuthResponseStatus) {
8
10
  UseMAuthResponseStatus["Idle"] = "idle";
@@ -10,50 +12,88 @@ var UseMAuthResponseStatus;
10
12
  UseMAuthResponseStatus["Success"] = "success";
11
13
  UseMAuthResponseStatus["Error"] = "error";
12
14
  })(UseMAuthResponseStatus || (UseMAuthResponseStatus = {}));
13
- function toStatus(hasAuthCode, tokenExchangeQuery) {
14
- if (!hasAuthCode) {
15
- return UseMAuthResponseStatus.Idle;
16
- }
17
- if (tokenExchangeQuery.isPending) {
18
- return UseMAuthResponseStatus.Loading;
19
- }
20
- if (tokenExchangeQuery.isError) {
21
- return UseMAuthResponseStatus.Error;
15
+ function buildTokenExchangeKey(params) {
16
+ return JSON.stringify([
17
+ params.serverUrl,
18
+ params.callbackUrl,
19
+ params.oauthClientId,
20
+ params.code,
21
+ params.storageKey,
22
+ params.authKey,
23
+ ]);
24
+ }
25
+ function toErrorMessage(error) {
26
+ if (error instanceof Error) {
27
+ return error.message;
22
28
  }
23
- return UseMAuthResponseStatus.Success;
29
+ return String(error);
24
30
  }
25
31
  ;
26
32
  export function useMAuthResponse({ serverUrl, callbackUrl, oauthClientId, authorizationCode, storage, auth, }) {
33
+ const serverUrlKey = toUrl(serverUrl ?? "http://api.meshcentral.com").toString();
34
+ const callbackUrlKey = toUrl(callbackUrl).toString();
27
35
  const code = authorizationCode ?? getAuthorizationCode() ?? undefined;
28
- const tokenExchangeQuery = useQuery({
29
- queryKey: [
30
- "mauth-response",
31
- toUrl(serverUrl).toString(),
32
- toUrl(callbackUrl).toString(),
36
+ const [status, setStatus] = useState(code === undefined ? UseMAuthResponseStatus.Idle : UseMAuthResponseStatus.Loading);
37
+ const [error, setError] = useState(null);
38
+ const tokenExchangeKey = code === undefined
39
+ ? null
40
+ : buildTokenExchangeKey({
41
+ serverUrl: serverUrlKey,
42
+ callbackUrl: callbackUrlKey,
33
43
  oauthClientId,
34
44
  code,
35
- ],
36
- enabled: code !== undefined,
37
- retry: false,
38
- refetchOnReconnect: false,
39
- refetchOnWindowFocus: false,
40
- queryFn: async ({ signal }) => {
41
- if (code === undefined) {
42
- throw new Error("Missing authorization code");
43
- }
44
- await exchangeToken({
45
- serverUrl,
45
+ storageKey: getReferenceKey(storage, "default-storage"),
46
+ authKey: getReferenceKey(auth, "default-auth"),
47
+ });
48
+ useEffect(() => {
49
+ if (code === undefined || tokenExchangeKey === null) {
50
+ setStatus(UseMAuthResponseStatus.Idle);
51
+ setError(null);
52
+ return;
53
+ }
54
+ let cancelled = false;
55
+ setStatus(UseMAuthResponseStatus.Loading);
56
+ setError(null);
57
+ let tokenExchange = inFlightTokenExchanges.get(tokenExchangeKey);
58
+ if (!tokenExchange) {
59
+ tokenExchange = exchangeToken({
60
+ serverUrl: serverUrlKey,
46
61
  callbackUrl,
47
62
  oauthClientId,
48
63
  authorizationCode: code,
49
64
  storage,
50
65
  auth,
51
- }, signal);
52
- return null;
53
- },
54
- });
55
- return {
56
- status: toStatus(code !== undefined, tokenExchangeQuery),
57
- error: tokenExchangeQuery.error?.message ?? null,
58
- };
66
+ }).finally(() => {
67
+ inFlightTokenExchanges.delete(tokenExchangeKey);
68
+ });
69
+ inFlightTokenExchanges.set(tokenExchangeKey, tokenExchange);
70
+ }
71
+ tokenExchange
72
+ .then(() => {
73
+ if (cancelled) {
74
+ return;
75
+ }
76
+ setStatus(UseMAuthResponseStatus.Success);
77
+ setError(null);
78
+ })
79
+ .catch((exchangeError) => {
80
+ if (cancelled) {
81
+ return;
82
+ }
83
+ setStatus(UseMAuthResponseStatus.Error);
84
+ setError(toErrorMessage(exchangeError));
85
+ });
86
+ return () => {
87
+ cancelled = true;
88
+ };
89
+ }, [
90
+ auth,
91
+ callbackUrl,
92
+ code,
93
+ oauthClientId,
94
+ serverUrl,
95
+ storage,
96
+ tokenExchangeKey,
97
+ ]);
98
+ return { status, error };
59
99
  }
@@ -0,0 +1 @@
1
+ export declare function getReferenceKey(value: object | null | undefined, fallback: string): string;
@@ -0,0 +1,15 @@
1
+ const objectIds = new WeakMap();
2
+ let nextObjectId = 1;
3
+ export function getReferenceKey(value, fallback) {
4
+ if (!value) {
5
+ return fallback;
6
+ }
7
+ const existing = objectIds.get(value);
8
+ if (existing !== undefined) {
9
+ return `${fallback}:${existing}`;
10
+ }
11
+ const id = nextObjectId;
12
+ nextObjectId += 1;
13
+ objectIds.set(value, id);
14
+ return `${fallback}:${id}`;
15
+ }
@@ -0,0 +1,35 @@
1
+ import { MeshagentAuth } from "@meshagent/meshagent-ts-auth";
2
+ import type { StorageLike, MeshagentUser } from "@meshagent/meshagent-ts-auth";
3
+ export declare function buildOAuthAuthorizeUrl(params: {
4
+ serverUrl: string | URL;
5
+ callbackUrl: string | URL;
6
+ oauthClientId: string;
7
+ scope?: string;
8
+ provider?: string | null;
9
+ extraQueryParams?: Record<string, string>;
10
+ storage?: StorageLike;
11
+ }): Promise<URL>;
12
+ export interface AuthRenderProps {
13
+ failed: unknown;
14
+ isCancelled: boolean;
15
+ isSigningIn: boolean;
16
+ refreshing: boolean;
17
+ user: MeshagentUser | null;
18
+ signIn: (provider?: string | null) => Promise<void>;
19
+ refresh: () => void;
20
+ }
21
+ export interface UseAuthProps {
22
+ serverUrl?: string | URL;
23
+ callbackUrl?: string | URL;
24
+ oauthClientId: string;
25
+ scope?: string;
26
+ provider?: string;
27
+ extraQueryParams?: Record<string, string>;
28
+ storage?: StorageLike;
29
+ auth?: MeshagentAuth;
30
+ autoSignIn?: boolean;
31
+ }
32
+ export interface UseAuthResult extends AuthRenderProps {
33
+ isLoginLaunched: boolean;
34
+ }
35
+ export declare function useAuth(props: UseAuthProps): UseAuthResult;