@meshagent/meshagent-react-auth 0.36.3 → 0.37.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.
@@ -0,0 +1,482 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildOAuthAuthorizeUrl = buildOAuthAuthorizeUrl;
4
+ exports.useAuth = useAuth;
5
+ const react_1 = require("react");
6
+ const meshagent_ts_auth_1 = require("@meshagent/meshagent-ts-auth");
7
+ const use_meshagent_auth_1 = require("./use-meshagent-auth");
8
+ const reference_key_1 = require("./reference-key");
9
+ const DEFAULT_SERVER_URL = "https://api.meshagent.com";
10
+ function toUrl(value) {
11
+ return value instanceof URL ? value : new URL(value);
12
+ }
13
+ function getCurrentUrl() {
14
+ if (typeof window === "undefined") {
15
+ throw new Error("useAuth requires a browser environment");
16
+ }
17
+ return new URL(window.location.href);
18
+ }
19
+ function clearMeshagentAuthQueryParams(url) {
20
+ url.searchParams.delete("code");
21
+ url.searchParams.delete("scope");
22
+ url.searchParams.delete("state");
23
+ url.searchParams.delete("error");
24
+ url.searchParams.delete("error_description");
25
+ url.searchParams.delete("error_uri");
26
+ }
27
+ function getResolvedCallbackUrl(callbackUrl) {
28
+ if (callbackUrl) {
29
+ return toUrl(callbackUrl);
30
+ }
31
+ const currentUrl = getCurrentUrl();
32
+ clearMeshagentAuthQueryParams(currentUrl);
33
+ return currentUrl;
34
+ }
35
+ function getCallbackUrlKey(callbackUrl) {
36
+ if (callbackUrl) {
37
+ return toUrl(callbackUrl).toString();
38
+ }
39
+ if (typeof window === "undefined") {
40
+ return "__current__";
41
+ }
42
+ return getResolvedCallbackUrl().toString();
43
+ }
44
+ function replaceCurrentUrlWithoutAuthParams() {
45
+ if (typeof window === "undefined" || typeof window.history.replaceState !== "function") {
46
+ return;
47
+ }
48
+ const currentUrl = getCurrentUrl();
49
+ clearMeshagentAuthQueryParams(currentUrl);
50
+ window.history.replaceState(window.history.state, "", currentUrl.toString());
51
+ }
52
+ function isMeshagentCallbackUrl(callbackUrl) {
53
+ if (typeof window === "undefined") {
54
+ return false;
55
+ }
56
+ const currentUrl = getCurrentUrl();
57
+ if (!currentUrl.searchParams.has("code")) {
58
+ return false;
59
+ }
60
+ const expectedCallbackUrl = new URL(callbackUrl);
61
+ clearMeshagentAuthQueryParams(currentUrl);
62
+ clearMeshagentAuthQueryParams(expectedCallbackUrl);
63
+ return currentUrl.toString() === expectedCallbackUrl.toString();
64
+ }
65
+ class UserProfileFetchError extends Error {
66
+ constructor(status, message) {
67
+ super(message);
68
+ Object.defineProperty(this, "status", {
69
+ enumerable: true,
70
+ configurable: true,
71
+ writable: true,
72
+ value: void 0
73
+ });
74
+ this.name = "UserProfileFetchError";
75
+ this.status = status;
76
+ }
77
+ }
78
+ function isAuthProfileError(error) {
79
+ return error instanceof UserProfileFetchError && (error.status === 401 || error.status === 403);
80
+ }
81
+ async function fetchUserProfile(params) {
82
+ const response = await fetch(new URL("/accounts/profiles/me", toUrl(params.serverUrl)), {
83
+ signal: params.signal,
84
+ headers: {
85
+ "Authorization": `Bearer ${params.accessToken}`,
86
+ "Accept": "application/json",
87
+ },
88
+ });
89
+ if (!response.ok) {
90
+ throw new UserProfileFetchError(response.status, `Failed to fetch user profile: ${response.status} ${await response.text()}`);
91
+ }
92
+ return await response.json();
93
+ }
94
+ function serializeExtraQueryParams(extraQueryParams) {
95
+ return JSON.stringify(Object.entries(extraQueryParams ?? {}).sort(([left], [right]) => left.localeCompare(right)));
96
+ }
97
+ function normalizeExtraQueryParams(extraQueryParams) {
98
+ const entries = Object.entries(extraQueryParams ?? {}).sort(([left], [right]) => left.localeCompare(right));
99
+ return entries.length === 0 ? undefined : Object.fromEntries(entries);
100
+ }
101
+ function buildSignInKey(params) {
102
+ return JSON.stringify([
103
+ params.serverUrl,
104
+ params.callbackUrl,
105
+ params.oauthClientId,
106
+ params.scope,
107
+ params.provider,
108
+ params.extraQueryParamsKey,
109
+ params.storageKey,
110
+ ]);
111
+ }
112
+ function buildTokenExchangeKey(params) {
113
+ return JSON.stringify([
114
+ params.serverUrl,
115
+ params.callbackUrl,
116
+ params.oauthClientId,
117
+ params.authorizationCode,
118
+ params.storageKey,
119
+ params.authKey,
120
+ ]);
121
+ }
122
+ const inFlightSignIns = new Map();
123
+ const inFlightTokenExchanges = new Map();
124
+ function launchSignInOnce(key, params) {
125
+ const existing = inFlightSignIns.get(key);
126
+ if (existing) {
127
+ return existing;
128
+ }
129
+ const signIn = launchSignIn(params).finally(() => {
130
+ inFlightSignIns.delete(key);
131
+ });
132
+ inFlightSignIns.set(key, signIn);
133
+ return signIn;
134
+ }
135
+ function exchangeTokenOnce(key, params) {
136
+ const existing = inFlightTokenExchanges.get(key);
137
+ if (existing) {
138
+ return existing;
139
+ }
140
+ const tokenExchange = (0, meshagent_ts_auth_1.exchangeToken)({
141
+ serverUrl: params.serverUrl,
142
+ callbackUrl: params.callbackUrl,
143
+ oauthClientId: params.oauthClientId,
144
+ authorizationCode: params.authorizationCode,
145
+ storage: params.storage,
146
+ auth: params.auth,
147
+ }, params.signal)
148
+ .then(async () => {
149
+ const cache = new meshagent_ts_auth_1.LocalStoragePkceCache(params.storage);
150
+ await cache.clearVerifier();
151
+ replaceCurrentUrlWithoutAuthParams();
152
+ })
153
+ .finally(() => {
154
+ inFlightTokenExchanges.delete(key);
155
+ });
156
+ inFlightTokenExchanges.set(key, tokenExchange);
157
+ return tokenExchange;
158
+ }
159
+ async function buildOAuthAuthorizeUrl(params) {
160
+ const serverUrl = toUrl(params.serverUrl);
161
+ const callbackUrl = toUrl(params.callbackUrl);
162
+ const generator = new meshagent_ts_auth_1.PkceGenerator();
163
+ const pair = await generator.generate();
164
+ const cache = new meshagent_ts_auth_1.LocalStoragePkceCache(params.storage ?? (0, meshagent_ts_auth_1.getDefaultStorage)());
165
+ await cache.saveVerifier(pair.codeVerifier);
166
+ const query = new URLSearchParams({
167
+ ...(params.extraQueryParams ?? {}),
168
+ scope: params.scope ?? "profile",
169
+ client_id: params.oauthClientId,
170
+ code_challenge: pair.codeChallenge,
171
+ response_type: "code",
172
+ redirect_uri: callbackUrl.toString(),
173
+ prompt: "select_account",
174
+ });
175
+ if (params.provider) {
176
+ query.set("provider", params.provider);
177
+ }
178
+ return new URL(`/oauth/authorize?${query.toString()}`, serverUrl);
179
+ }
180
+ async function launchSignIn(params) {
181
+ const authorizeUrl = await buildOAuthAuthorizeUrl({
182
+ serverUrl: params.serverUrl,
183
+ callbackUrl: params.callbackUrl,
184
+ oauthClientId: params.oauthClientId,
185
+ scope: params.scope,
186
+ provider: params.provider,
187
+ extraQueryParams: params.extraQueryParams,
188
+ storage: params.storage,
189
+ });
190
+ if (typeof window === "undefined") {
191
+ throw new Error("useAuth requires a browser environment");
192
+ }
193
+ window.location.assign(authorizeUrl.toString());
194
+ }
195
+ function useAuth(props) {
196
+ const serverUrl = toUrl(props.serverUrl ?? DEFAULT_SERVER_URL).toString();
197
+ const oauthClientId = props.oauthClientId;
198
+ const scope = props.scope ?? "profile";
199
+ const defaultProvider = props.provider ?? null;
200
+ const storage = props.storage ?? (0, meshagent_ts_auth_1.getDefaultStorage)();
201
+ const auth = props.auth ?? meshagent_ts_auth_1.meshagentAuth;
202
+ const authSnapshot = (0, use_meshagent_auth_1.useMeshagentAuth)(auth);
203
+ const authKey = (0, reference_key_1.getReferenceKey)(auth, "default-auth");
204
+ const storageKey = (0, reference_key_1.getReferenceKey)(storage, "default-storage");
205
+ const callbackUrlKey = (0, react_1.useMemo)(() => getCallbackUrlKey(props.callbackUrl), [props.callbackUrl]);
206
+ const extraQueryParamsKey = (0, react_1.useMemo)(() => serializeExtraQueryParams(props.extraQueryParams), [props.extraQueryParams]);
207
+ const normalizedExtraQueryParams = (0, react_1.useMemo)(() => normalizeExtraQueryParams(props.extraQueryParams), [extraQueryParamsKey]);
208
+ const autoSignIn = props.autoSignIn ?? true;
209
+ const authRef = (0, react_1.useRef)(auth);
210
+ const storageRef = (0, react_1.useRef)(storage);
211
+ const exchangedAccessTokenRef = (0, react_1.useRef)(null);
212
+ const [signInFailed, setSignInFailed] = (0, react_1.useState)(null);
213
+ const [isSigningIn, setIsSigningIn] = (0, react_1.useState)(false);
214
+ const [refreshVersion, setRefreshVersion] = (0, react_1.useState)(0);
215
+ const [loginState, setLoginState] = (0, react_1.useState)({
216
+ failed: null,
217
+ isLoginLaunched: false,
218
+ refreshing: true,
219
+ });
220
+ authRef.current = auth;
221
+ storageRef.current = storage;
222
+ const signIn = async (provider) => {
223
+ const selectedProvider = provider === undefined ? defaultProvider : provider;
224
+ const callbackUrl = callbackUrlKey;
225
+ const key = buildSignInKey({
226
+ serverUrl,
227
+ callbackUrl,
228
+ oauthClientId,
229
+ scope,
230
+ provider: selectedProvider ?? null,
231
+ extraQueryParamsKey,
232
+ storageKey,
233
+ });
234
+ setIsSigningIn(true);
235
+ setSignInFailed(null);
236
+ try {
237
+ await launchSignInOnce(key, {
238
+ serverUrl,
239
+ callbackUrl,
240
+ oauthClientId,
241
+ scope,
242
+ provider: selectedProvider,
243
+ extraQueryParams: normalizedExtraQueryParams,
244
+ storage,
245
+ });
246
+ }
247
+ catch (error) {
248
+ setSignInFailed(error);
249
+ setIsSigningIn(false);
250
+ }
251
+ };
252
+ (0, react_1.useEffect)(() => {
253
+ let cancelled = false;
254
+ const controller = new AbortController();
255
+ setLoginState((previous) => previous.refreshing
256
+ ? previous
257
+ : {
258
+ ...previous,
259
+ refreshing: true,
260
+ });
261
+ const run = async () => {
262
+ const currentAuth = authRef.current;
263
+ const currentStorage = storageRef.current;
264
+ const session = new meshagent_ts_auth_1.OAuthSessionManager({
265
+ serverUrl,
266
+ clientId: oauthClientId,
267
+ auth: currentAuth,
268
+ });
269
+ if (isMeshagentCallbackUrl(callbackUrlKey)) {
270
+ const authorizationCode = (0, meshagent_ts_auth_1.getAuthorizationCode)();
271
+ if (authorizationCode) {
272
+ const callbackUrl = callbackUrlKey;
273
+ const key = buildTokenExchangeKey({
274
+ serverUrl,
275
+ callbackUrl,
276
+ oauthClientId,
277
+ authorizationCode,
278
+ storageKey,
279
+ authKey,
280
+ });
281
+ try {
282
+ await exchangeTokenOnce(key, {
283
+ serverUrl,
284
+ callbackUrl,
285
+ oauthClientId,
286
+ authorizationCode,
287
+ storage: currentStorage,
288
+ auth: currentAuth,
289
+ signal: controller.signal,
290
+ });
291
+ if (cancelled || controller.signal.aborted) {
292
+ return;
293
+ }
294
+ exchangedAccessTokenRef.current = currentAuth.getAccessToken();
295
+ setSignInFailed(null);
296
+ setIsSigningIn(false);
297
+ setLoginState({
298
+ failed: null,
299
+ isLoginLaunched: false,
300
+ refreshing: false,
301
+ });
302
+ return;
303
+ }
304
+ catch (error) {
305
+ if (cancelled || controller.signal.aborted) {
306
+ return;
307
+ }
308
+ setIsSigningIn(false);
309
+ setLoginState({
310
+ failed: error,
311
+ isLoginLaunched: true,
312
+ refreshing: false,
313
+ });
314
+ return;
315
+ }
316
+ }
317
+ }
318
+ if (!authSnapshot.accessToken) {
319
+ setSignInFailed(null);
320
+ if (autoSignIn) {
321
+ const callbackUrl = callbackUrlKey;
322
+ const key = buildSignInKey({
323
+ serverUrl,
324
+ callbackUrl,
325
+ oauthClientId,
326
+ scope,
327
+ provider: defaultProvider,
328
+ extraQueryParamsKey,
329
+ storageKey,
330
+ });
331
+ setIsSigningIn(true);
332
+ try {
333
+ await launchSignInOnce(key, {
334
+ serverUrl,
335
+ callbackUrl,
336
+ oauthClientId,
337
+ scope,
338
+ provider: defaultProvider,
339
+ extraQueryParams: normalizedExtraQueryParams,
340
+ storage: currentStorage,
341
+ });
342
+ if (cancelled) {
343
+ return;
344
+ }
345
+ setLoginState({
346
+ failed: null,
347
+ isLoginLaunched: false,
348
+ refreshing: false,
349
+ });
350
+ return;
351
+ }
352
+ catch (error) {
353
+ if (cancelled) {
354
+ return;
355
+ }
356
+ setIsSigningIn(false);
357
+ setLoginState({
358
+ failed: error,
359
+ isLoginLaunched: true,
360
+ refreshing: false,
361
+ });
362
+ return;
363
+ }
364
+ }
365
+ if (cancelled) {
366
+ return;
367
+ }
368
+ setIsSigningIn(false);
369
+ setLoginState({
370
+ failed: null,
371
+ isLoginLaunched: true,
372
+ refreshing: false,
373
+ });
374
+ return;
375
+ }
376
+ if (exchangedAccessTokenRef.current !== null
377
+ && authSnapshot.accessToken === exchangedAccessTokenRef.current) {
378
+ exchangedAccessTokenRef.current = null;
379
+ if (cancelled) {
380
+ return;
381
+ }
382
+ setSignInFailed(null);
383
+ setIsSigningIn(false);
384
+ setLoginState({
385
+ failed: null,
386
+ isLoginLaunched: false,
387
+ refreshing: false,
388
+ });
389
+ return;
390
+ }
391
+ let accessToken;
392
+ try {
393
+ accessToken = await session.getValidAccessTokenOrThrow();
394
+ }
395
+ catch (error) {
396
+ currentAuth.signOut();
397
+ if (cancelled) {
398
+ return;
399
+ }
400
+ setIsSigningIn(false);
401
+ setLoginState({
402
+ failed: error,
403
+ isLoginLaunched: true,
404
+ refreshing: false,
405
+ });
406
+ return;
407
+ }
408
+ try {
409
+ const user = await fetchUserProfile({
410
+ serverUrl,
411
+ accessToken,
412
+ signal: controller.signal,
413
+ });
414
+ currentAuth.setUser(user);
415
+ if (cancelled) {
416
+ return;
417
+ }
418
+ setSignInFailed(null);
419
+ setIsSigningIn(false);
420
+ setLoginState({
421
+ failed: null,
422
+ isLoginLaunched: false,
423
+ refreshing: false,
424
+ });
425
+ }
426
+ catch (error) {
427
+ if (cancelled || controller.signal.aborted) {
428
+ return;
429
+ }
430
+ setIsSigningIn(false);
431
+ if (isAuthProfileError(error)) {
432
+ currentAuth.signOut();
433
+ setLoginState({
434
+ failed: error,
435
+ isLoginLaunched: true,
436
+ refreshing: false,
437
+ });
438
+ return;
439
+ }
440
+ setLoginState({
441
+ failed: error,
442
+ isLoginLaunched: false,
443
+ refreshing: false,
444
+ });
445
+ }
446
+ };
447
+ void run();
448
+ return () => {
449
+ cancelled = true;
450
+ controller.abort();
451
+ };
452
+ }, [
453
+ authKey,
454
+ authSnapshot.accessToken,
455
+ autoSignIn,
456
+ callbackUrlKey,
457
+ defaultProvider,
458
+ extraQueryParamsKey,
459
+ oauthClientId,
460
+ refreshVersion,
461
+ scope,
462
+ serverUrl,
463
+ storageKey,
464
+ ]);
465
+ const refresh = () => {
466
+ setSignInFailed(null);
467
+ setLoginState((previous) => ({ ...previous, refreshing: true }));
468
+ setRefreshVersion((version) => version + 1);
469
+ };
470
+ const failed = signInFailed ?? (loginState.refreshing ? null : loginState.failed ?? null);
471
+ const isLoginLaunched = signInFailed !== null ? true : loginState.isLoginLaunched;
472
+ return {
473
+ failed,
474
+ isCancelled: false,
475
+ isSigningIn,
476
+ refreshing: loginState.refreshing,
477
+ user: authSnapshot.user,
478
+ signIn,
479
+ refresh,
480
+ isLoginLaunched,
481
+ };
482
+ }
@@ -0,0 +1,13 @@
1
+ export type UseEnsureLoginStatus = "loading" | "success" | "error";
2
+ export interface UseEnsureLoginProps {
3
+ serverUrl?: string | URL;
4
+ callbackUrl?: string | URL;
5
+ oauthClientId: string;
6
+ provider?: string;
7
+ }
8
+ export interface UseEnsureLoginResult {
9
+ status: UseEnsureLoginStatus;
10
+ error: string | null;
11
+ refresh: () => void;
12
+ }
13
+ export declare function useEnsureLogin(props: UseEnsureLoginProps): UseEnsureLoginResult;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useEnsureLogin = useEnsureLogin;
4
+ const use_auth_1 = require("./use-auth");
5
+ function toErrorMessage(error) {
6
+ if (error instanceof Error) {
7
+ return error.message;
8
+ }
9
+ return String(error);
10
+ }
11
+ function useEnsureLogin(props) {
12
+ const auth = (0, use_auth_1.useAuth)({
13
+ serverUrl: props.serverUrl,
14
+ callbackUrl: props.callbackUrl,
15
+ oauthClientId: props.oauthClientId,
16
+ provider: props.provider,
17
+ scope: "profile",
18
+ autoSignIn: true,
19
+ });
20
+ return {
21
+ status: auth.failed
22
+ ? "error"
23
+ : auth.refreshing || auth.user === null
24
+ ? "loading"
25
+ : "success",
26
+ error: auth.failed === null ? null : toErrorMessage(auth.failed),
27
+ refresh: auth.refresh,
28
+ };
29
+ }
@@ -1,5 +1,4 @@
1
- import { MeshagentAuth } from "@meshagent/meshagent-ts-auth";
2
- import type { StorageLike, MeshagentUser } from "@meshagent/meshagent-ts-auth";
1
+ import type { MeshagentAuth, MeshagentUser, StorageLike } from "@meshagent/meshagent-ts-auth";
3
2
  export interface LoginScopeRenderProps {
4
3
  failed: unknown;
5
4
  isCancelled: boolean;
@@ -9,8 +8,8 @@ export interface LoginScopeRenderProps {
9
8
  signIn: (provider?: string | null) => Promise<void>;
10
9
  refresh: () => void;
11
10
  }
12
- export interface UseLoginScopeOptions {
13
- serverUrl: string | URL;
11
+ export interface LoginScopeProps {
12
+ serverUrl?: string | URL;
14
13
  callbackUrl: string | URL;
15
14
  oauthClientId: string;
16
15
  scope?: string;
@@ -22,13 +21,4 @@ export interface UseLoginScopeOptions {
22
21
  export interface UseLoginScopeResult extends LoginScopeRenderProps {
23
22
  isLoginLaunched: boolean;
24
23
  }
25
- export declare function buildOAuthAuthorizeUrl(params: {
26
- serverUrl: string | URL;
27
- callbackUrl: string | URL;
28
- oauthClientId: string;
29
- scope?: string;
30
- provider?: string | null;
31
- extraQueryParams?: Record<string, string>;
32
- storage?: StorageLike;
33
- }): Promise<URL>;
34
- export declare function useLoginScope(options: UseLoginScopeOptions): UseLoginScopeResult;
24
+ export declare function useLoginScope(props: LoginScopeProps): UseLoginScopeResult;