oidc-spa 8.2.10 → 8.2.12

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.
Files changed (32) hide show
  1. package/README.md +194 -22
  2. package/core/createOidc.js +1 -1
  3. package/core/earlyInit.js +2 -2
  4. package/core/earlyInit.js.map +1 -1
  5. package/core/iframeMessageProtection.d.ts +5 -2
  6. package/core/iframeMessageProtection.js +44 -19
  7. package/core/iframeMessageProtection.js.map +1 -1
  8. package/esm/core/createOidc.js +1 -1
  9. package/esm/core/earlyInit.js +3 -3
  10. package/esm/core/earlyInit.js.map +1 -1
  11. package/esm/core/iframeMessageProtection.d.ts +5 -2
  12. package/esm/core/iframeMessageProtection.js +43 -17
  13. package/esm/core/iframeMessageProtection.js.map +1 -1
  14. package/esm/react-spa/createOidcSpaApi.js +1 -1
  15. package/esm/react-spa/createOidcSpaApi.js.map +1 -1
  16. package/esm/react-spa/types.d.ts +5 -5
  17. package/esm/tanstack-start/react/createOidcSpaApi.js +107 -88
  18. package/esm/tanstack-start/react/createOidcSpaApi.js.map +1 -1
  19. package/esm/tanstack-start/react/types.d.ts +43 -21
  20. package/esm/tanstack-start/react/withHandlingOidcPostLoginNavigation.js +13 -2
  21. package/esm/tanstack-start/react/withHandlingOidcPostLoginNavigation.js.map +1 -1
  22. package/package.json +1 -1
  23. package/react-spa/createOidcSpaApi.js +1 -1
  24. package/react-spa/createOidcSpaApi.js.map +1 -1
  25. package/react-spa/types.d.ts +5 -5
  26. package/src/core/earlyInit.ts +5 -7
  27. package/src/core/iframeMessageProtection.ts +55 -22
  28. package/src/react-spa/createOidcSpaApi.tsx +1 -1
  29. package/src/react-spa/types.tsx +5 -5
  30. package/src/tanstack-start/react/createOidcSpaApi.tsx +145 -153
  31. package/src/tanstack-start/react/types.tsx +40 -29
  32. package/src/tanstack-start/react/withHandlingOidcPostLoginNavigation.tsx +13 -2
@@ -1,15 +1,8 @@
1
- import {
2
- useState,
3
- useEffect,
4
- type ReactNode,
5
- createContext,
6
- useContext,
7
- type ComponentType
8
- } from "react";
1
+ import { useState, useEffect, type ReactNode, useReducer, type ComponentType } from "react";
9
2
  import type {
10
3
  CreateValidateAndGetAccessTokenClaims,
11
4
  OidcSpaApi,
12
- CreateOidcComponent,
5
+ UseOidc,
13
6
  GetOidc,
14
7
  ParamsOfBootstrap,
15
8
  OidcServerContext
@@ -62,11 +55,11 @@ export function createOidcSpaApi<
62
55
  Oidc_core<DecodedIdToken> | OidcInitializationError
63
56
  >();
64
57
 
65
- const evtAutoLogoutState = createStatefulEvt<
66
- CreateOidcComponent.Oidc.LoggedIn<unknown>["autoLogoutState"]
67
- >(() => ({
68
- shouldDisplayWarning: false
69
- }));
58
+ const evtAutoLogoutState = createStatefulEvt<UseOidc.Oidc.LoggedIn<unknown>["autoLogoutState"]>(
59
+ () => ({
60
+ shouldDisplayWarning: false
61
+ })
62
+ );
70
63
 
71
64
  dOidcCoreOrInitializationError.pr.then(oidcCoreOrInitializationError => {
72
65
  const { hasResolved, value: paramsOfBootstrap } = dParamsOfBootstrap.getState();
@@ -94,7 +87,7 @@ export function createOidcSpaApi<
94
87
  }
95
88
 
96
89
  oidcCore.subscribeToAutoLogoutCountdown(({ secondsLeft }) => {
97
- const newState: CreateOidcComponent.Oidc.LoggedIn<unknown>["autoLogoutState"] = (() => {
90
+ const newState: UseOidc.Oidc.LoggedIn<unknown>["autoLogoutState"] = (() => {
98
91
  if (secondsLeft === undefined) {
99
92
  return {
100
93
  shouldDisplayWarning: false
@@ -121,13 +114,116 @@ export function createOidcSpaApi<
121
114
  });
122
115
  });
123
116
 
124
- function useOidc(): CreateOidcComponent.Oidc<DecodedIdToken> {
117
+ function useOidc(params?: {
118
+ assert?: "user logged in" | "user not logged in" | "init completed";
119
+ }): UseOidc.Oidc<DecodedIdToken> {
120
+ const { assert: assert_params } = params ?? {};
121
+
122
+ /*
123
+ if (!isBrowser) {
124
+ throw new Error(
125
+ [
126
+ "oidc-spa: useOidc() can't be used on the server.",
127
+ "You can prevent this component from rendering on the server",
128
+ "by wrapping it into <OidcInitializationGate />"
129
+ ].join(" ")
130
+ );
131
+ }
132
+ */
133
+
125
134
  const { hasResolved, value: oidcCore } = dOidcCoreOrInitializationError.getState();
126
135
 
127
- assert(hasResolved);
128
- assert(!(oidcCore instanceof OidcInitializationError));
136
+ if (oidcCore instanceof OidcInitializationError) {
137
+ throw oidcCore;
138
+ }
139
+
140
+ check_assertion: {
141
+ if (assert_params === undefined) {
142
+ break check_assertion;
143
+ }
144
+
145
+ if (assert_params === "init completed") {
146
+ if (!hasResolved) {
147
+ throw new Error(
148
+ [
149
+ "oidc-spa: There is a logic error in the application.",
150
+ "you called useOidc({ assert: 'init completed' }) but",
151
+ ...(isBrowser
152
+ ? [
153
+ "the component making this call was rendered before",
154
+ "the oidc initialization has completed."
155
+ ]
156
+ : ["we are on the server, this assertion will always be wrong."]),
157
+ "\nTo avoid this error make sure you call this within",
158
+ "<OidcInitializationGate /> or remove the assertion and check",
159
+ "the hasInitCompleted property."
160
+ ].join(" ")
161
+ );
162
+ }
163
+
164
+ break check_assertion;
165
+ }
166
+
167
+ if (!hasResolved) {
168
+ throw new Error(
169
+ [
170
+ "oidc-spa: There is a logic error in the application.",
171
+ `you called useOidc({ assert: "${assert_params}" }) but`,
172
+ "initialization hasn't completed yet, we don't know yet if the",
173
+ "user is logged in or not."
174
+ ].join(" ")
175
+ );
176
+ }
177
+
178
+ const getMessage = (v: string) =>
179
+ [
180
+ "oidc-spa: There is a logic error in the application.",
181
+ `If this component is mounted the user is supposed ${v}.`,
182
+ "An explicit assertion was made in this sense."
183
+ ].join(" ");
184
+
185
+ switch (assert_params) {
186
+ case "user logged in":
187
+ if (!oidcCore.isUserLoggedIn) {
188
+ throw new Error(getMessage("to be logged in but currently they arn't"));
189
+ }
190
+ break;
191
+ case "user not logged in":
192
+ if (oidcCore.isUserLoggedIn) {
193
+ throw new Error(getMessage("not to be logged in but currently they are"));
194
+ }
195
+ break;
196
+ default:
197
+ assert<Equals<typeof assert_params, never>>(false);
198
+ }
199
+ }
200
+
201
+ const [, reRender] = useReducer(n => n + 1, 0);
202
+
203
+ useEffect(() => {
204
+ if (hasResolved) {
205
+ return;
206
+ }
207
+
208
+ let isActive = true;
209
+
210
+ dOidcCoreOrInitializationError.pr.then(() => {
211
+ if (!isActive) {
212
+ return;
213
+ }
214
+ reRender();
215
+ });
216
+
217
+ return () => {
218
+ isActive = false;
219
+ };
220
+ }, []);
129
221
 
130
222
  const [, reRenderIfDecodedIdTokenChanged] = useState<DecodedIdToken | undefined>(() => {
223
+ if (oidcCore === undefined) {
224
+ return undefined;
225
+ }
226
+
131
227
  if (!oidcCore.isUserLoggedIn) {
132
228
  return undefined;
133
229
  }
@@ -137,6 +233,10 @@ export function createOidcSpaApi<
137
233
  const [evtIsDecodedIdTokenUsed] = useState(() => createStatefulEvt<boolean>(() => false));
138
234
 
139
235
  useEffect(() => {
236
+ if (oidcCore === undefined) {
237
+ return undefined;
238
+ }
239
+
140
240
  if (!oidcCore.isUserLoggedIn) {
141
241
  return;
142
242
  }
@@ -211,8 +311,30 @@ export function createOidcSpaApi<
211
311
  };
212
312
  }, []);
213
313
 
314
+ const [hasHydrated, setHasHydratedToTrue] = useReducer(
315
+ () => true,
316
+ assert_params !== undefined ? undefined : false
317
+ );
318
+
319
+ useEffect(() => {
320
+ if (hasHydrated === undefined) {
321
+ return;
322
+ }
323
+ setHasHydratedToTrue();
324
+ }, []);
325
+
326
+ if (!hasResolved || hasHydrated === false) {
327
+ return id<UseOidc.Oidc.InitNotCompleted>({
328
+ hasInitCompleted: false,
329
+ autoLogoutState: {
330
+ shouldDisplayWarning: false
331
+ }
332
+ });
333
+ }
334
+
214
335
  if (!oidcCore.isUserLoggedIn) {
215
- return id<CreateOidcComponent.Oidc.NotLoggedIn>({
336
+ return id<UseOidc.Oidc.NotLoggedIn>({
337
+ hasInitCompleted: true,
216
338
  isUserLoggedIn: false,
217
339
  initializationError: oidcCore.initializationError,
218
340
  issuerUri: oidcCore.params.issuerUri,
@@ -227,7 +349,8 @@ export function createOidcSpaApi<
227
349
  });
228
350
  }
229
351
 
230
- return id<CreateOidcComponent.Oidc.LoggedIn<DecodedIdToken>>({
352
+ return id<UseOidc.Oidc.LoggedIn<DecodedIdToken>>({
353
+ hasInitCompleted: true,
231
354
  isUserLoggedIn: true,
232
355
  get decodedIdToken() {
233
356
  evtIsDecodedIdTokenUsed.current = true;
@@ -248,139 +371,8 @@ export function createOidcSpaApi<
248
371
  });
249
372
  }
250
373
 
251
- const context_isFreeOfSsrHydrationConcern = createContext<boolean>(false);
252
-
253
- function createOidcComponent<Props extends Record<string, unknown>>(params: {
254
- assert?: "user logged in" | "user not logged in";
255
- pendingComponent?: (props: NoInfer<Props>) => ReactNode;
256
- component: (props: Props) => ReactNode;
257
- }): ((props: Props) => ReactNode) & {
258
- useOidc: () => CreateOidcComponent.Oidc<DecodedIdToken>;
259
- } {
260
- const {
261
- assert: assert_params,
262
- pendingComponent: PendingComponent,
263
- component: Component
264
- } = params;
265
-
266
- const checkAssertion =
267
- assert_params === undefined
268
- ? undefined
269
- : (params: { isUserLoggedIn: boolean }): void => {
270
- const { isUserLoggedIn } = params;
271
-
272
- switch (assert_params) {
273
- case "user not logged in":
274
- if (isUserLoggedIn) {
275
- throw new Error(
276
- [
277
- "oidc-spa: Asserted the user should not be logged in",
278
- "but they are. Check your control flow."
279
- ].join(" ")
280
- );
281
- }
282
- break;
283
- case "user logged in":
284
- if (!isUserLoggedIn) {
285
- throw new Error(
286
- [
287
- "oidc-spa: Asserted the user should be logged in",
288
- "but they arn't. Check your control flow."
289
- ].join(" ")
290
- );
291
- }
292
- break;
293
- default:
294
- assert<Equals<typeof assert_params, never>>;
295
- }
296
- };
297
-
298
- function ComponentWithOidc(props: Props) {
299
- const renderFallback = () =>
300
- PendingComponent === undefined ? null : <PendingComponent {...props} />;
301
-
302
- if (!isBrowser) {
303
- return renderFallback();
304
- }
305
-
306
- // NOTE: When the user assert that the user is logged in or not, they know.
307
- // if they knows it means that they learned it somewhere so we are post SSR.
308
- // Additionally, in autoLogin mode, the typedef don't allow this param to be provided.
309
- const isFreeOfSsrHydrationConcern =
310
- useContext(context_isFreeOfSsrHydrationConcern) || assert_params !== undefined;
311
-
312
- const [oidcCore, setOidcCore] = useState<Oidc_core<DecodedIdToken> | undefined>(() => {
313
- if (!isFreeOfSsrHydrationConcern) {
314
- return undefined;
315
- }
316
-
317
- const { hasResolved, value: oidcCore } = dOidcCoreOrInitializationError.getState();
318
-
319
- if (!hasResolved) {
320
- return undefined;
321
- }
322
-
323
- if (oidcCore instanceof OidcInitializationError) {
324
- return undefined;
325
- }
326
-
327
- checkAssertion?.({
328
- isUserLoggedIn: oidcCore.isUserLoggedIn
329
- });
330
-
331
- return oidcCore;
332
- });
333
-
334
- useEffect(() => {
335
- if (oidcCore !== undefined) {
336
- return;
337
- }
338
-
339
- let isActive = true;
340
-
341
- dOidcCoreOrInitializationError.pr.then(oidcCore => {
342
- if (!isActive) {
343
- return;
344
- }
345
-
346
- if (oidcCore instanceof OidcInitializationError) {
347
- return;
348
- }
349
-
350
- checkAssertion?.({
351
- isUserLoggedIn: oidcCore.isUserLoggedIn
352
- });
353
-
354
- setOidcCore(oidcCore);
355
- });
356
-
357
- return () => {
358
- isActive = false;
359
- };
360
- }, []);
361
-
362
- if (oidcCore === undefined) {
363
- return PendingComponent === undefined ? null : <PendingComponent {...props} />;
364
- }
365
-
366
- return (
367
- <context_isFreeOfSsrHydrationConcern.Provider value={true}>
368
- <Component {...props} />
369
- </context_isFreeOfSsrHydrationConcern.Provider>
370
- );
371
- }
372
-
373
- ComponentWithOidc.displayName = `${
374
- (Component as any).displayName ?? Component.name ?? "Component"
375
- }WithOidc`;
376
-
377
- ComponentWithOidc.useOidc = useOidc;
378
-
379
- return ComponentWithOidc;
380
- }
381
-
382
374
  async function getOidc(params?: {
383
- assert?: "user logged in" | "user not logged in";
375
+ assert?: "user logged in" | "user not logged in" | "init completed";
384
376
  }): Promise<GetOidc.Oidc<DecodedIdToken>> {
385
377
  if (!isBrowser) {
386
378
  throw new UnifiedClientRetryForSsrLoadersError(
@@ -1021,7 +1013,7 @@ export function createOidcSpaApi<
1021
1013
 
1022
1014
  // @ts-expect-error
1023
1015
  return {
1024
- createOidcComponent,
1016
+ useOidc,
1025
1017
  getOidc,
1026
1018
  bootstrapOidc,
1027
1019
  enforceLogin,
@@ -5,30 +5,34 @@ import type { GetterOrDirectValue } from "../../tools/GetterOrDirectValue";
5
5
  import type { OidcMetadata } from "../../core/OidcMetadata";
6
6
  import type { MaybeAsync } from "../../tools/MaybeAsync";
7
7
 
8
- export type CreateOidcComponent<DecodedIdToken> = <
9
- Assert extends "user logged in" | "user not logged in" | undefined,
10
- Props
11
- >(params: {
12
- assert?: Assert;
13
- pendingComponent?: Assert extends undefined ? (props: NoInfer<Props>) => ReactNode : undefined;
14
- component: (props: Props) => any;
15
- }) => ((props: Props) => ReactNode) & {
16
- useOidc: () => undefined extends Assert
17
- ? CreateOidcComponent.Oidc<DecodedIdToken>
18
- : "user logged in" extends Assert
19
- ? CreateOidcComponent.Oidc.LoggedIn<DecodedIdToken>
20
- : CreateOidcComponent.Oidc.NotLoggedIn;
8
+ export type UseOidc<DecodedIdToken> = {
9
+ (params?: { assert?: undefined }): UseOidc.Oidc<DecodedIdToken>;
10
+ (params: { assert: "user logged in" }): UseOidc.Oidc.LoggedIn<DecodedIdToken>;
11
+ (params: { assert: "user not logged in" }): UseOidc.Oidc.NotLoggedIn;
21
12
  };
22
13
 
23
- export namespace CreateOidcComponent {
24
- export type WithAutoLogin<DecodedIdToken> = <Props>(params: {
25
- pendingComponent?: (params: NoInfer<Props>) => ReactNode;
26
- component: (props: Props) => any;
27
- }) => ((props: Props) => ReactNode) & {
28
- useOidc: () => Oidc.LoggedIn<DecodedIdToken>;
29
- };
14
+ export namespace UseOidc {
15
+ export type WithAutoLogin<DecodedIdToken> = (params?: {
16
+ assert: "init completed";
17
+ }) => Oidc.LoggedIn<DecodedIdToken>;
30
18
 
31
19
  export type Oidc<DecodedIdToken> =
20
+ | (Oidc.InitNotCompleted & {
21
+ isUserLoggedIn?: never;
22
+ issuerUri?: never;
23
+ clientId?: never;
24
+ validRedirectUri?: never;
25
+
26
+ decodedIdToken?: never;
27
+ logout?: never;
28
+ renewTokens?: never;
29
+ goToAuthServer?: never;
30
+ backFromAuthServer?: never;
31
+ isNewBrowserSession?: never;
32
+
33
+ login?: never;
34
+ initializationError?: never;
35
+ })
32
36
  | (Oidc.NotLoggedIn & {
33
37
  decodedIdToken?: never;
34
38
  logout?: never;
@@ -43,13 +47,19 @@ export namespace CreateOidcComponent {
43
47
  });
44
48
 
45
49
  export namespace Oidc {
46
- type Common = {
50
+ export type InitNotCompleted = {
51
+ hasInitCompleted: false;
52
+ autoLogoutState: {
53
+ shouldDisplayWarning: false;
54
+ };
55
+ };
56
+
57
+ export type NotLoggedIn = {
58
+ hasInitCompleted: true;
59
+ isUserLoggedIn: false;
47
60
  issuerUri: string;
48
61
  clientId: string;
49
62
  validRedirectUri: string;
50
- };
51
-
52
- export type NotLoggedIn = Common & {
53
63
  login: (params?: {
54
64
  extraQueryParams?: Record<string, string | undefined>;
55
65
  redirectUrl?: string;
@@ -58,12 +68,15 @@ export namespace CreateOidcComponent {
58
68
  autoLogoutState: {
59
69
  shouldDisplayWarning: false;
60
70
  };
61
- isUserLoggedIn: false;
62
71
  initializationError: OidcInitializationError | undefined;
63
72
  };
64
73
 
65
- export type LoggedIn<DecodedIdToken> = Common & {
74
+ export type LoggedIn<DecodedIdToken> = {
75
+ hasInitCompleted: true;
66
76
  isUserLoggedIn: true;
77
+ issuerUri: string;
78
+ clientId: string;
79
+ validRedirectUri: string;
67
80
  decodedIdToken: DecodedIdToken;
68
81
  logout: Oidc_core.LoggedIn["logout"];
69
82
  renewTokens: Oidc_core.LoggedIn["renewTokens"];
@@ -419,9 +432,7 @@ export type OidcSpaApi<AutoLogin, DecodedIdToken, AccessTokenClaims> = {
419
432
  ParamsOfBootstrap<AutoLogin, DecodedIdToken, AccessTokenClaims>
420
433
  >
421
434
  ) => void;
422
- createOidcComponent: AutoLogin extends true
423
- ? CreateOidcComponent.WithAutoLogin<DecodedIdToken>
424
- : CreateOidcComponent<DecodedIdToken>;
435
+ useOidc: AutoLogin extends true ? UseOidc.WithAutoLogin<DecodedIdToken> : UseOidc<DecodedIdToken>;
425
436
  getOidc: AutoLogin extends true ? GetOidc.WithAutoLogin<DecodedIdToken> : GetOidc<DecodedIdToken>;
426
437
  } & (AccessTokenClaims extends undefined
427
438
  ? {}
@@ -20,8 +20,19 @@ export function withHandlingOidcPostLoginNavigation<Props extends Record<string,
20
20
  return;
21
21
  }
22
22
 
23
- router.navigate({ to: rootRelativeRedirectUrl, replace: true });
24
- rootRelativeRedirectUrl = undefined;
23
+ // Defer navigation to the next paint to avoid hydration mismatches.
24
+ // A double rAF schedules after hydration/paint without arbitrary timeouts.
25
+ requestAnimationFrame(() => {
26
+ requestAnimationFrame(() => {
27
+ if (rootRelativeRedirectUrl !== undefined) {
28
+ router.navigate({
29
+ to: rootRelativeRedirectUrl,
30
+ replace: true
31
+ });
32
+ rootRelativeRedirectUrl = undefined;
33
+ }
34
+ });
35
+ });
25
36
  }, []);
26
37
 
27
38
  return <Component {...props} />;