@mehdad67/apitogo 0.1.29 → 0.1.31

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/cli/cli.js CHANGED
@@ -4121,7 +4121,7 @@ import {
4121
4121
  // package.json
4122
4122
  var package_default = {
4123
4123
  name: "@mehdad67/apitogo",
4124
- version: "0.1.29",
4124
+ version: "0.1.31",
4125
4125
  type: "module",
4126
4126
  sideEffects: [
4127
4127
  "**/*.css",
@@ -20,6 +20,8 @@ export declare class DevPortalAuthenticationProvider extends CoreAuthenticationP
20
20
  private readonly redirectToAfterSignOut;
21
21
  private authMode;
22
22
  private backendAvailable;
23
+ private refreshGeneration;
24
+ private refreshInFlight;
23
25
  constructor({ apiBaseUrl, redirectToAfterSignIn, redirectToAfterSignUp, redirectToAfterSignOut, }: DevPortalAuthenticationConfig);
24
26
  initialize(_context: ZudokuContext): Promise<void>;
25
27
  onPageLoad: () => Promise<void>;
@@ -27,6 +29,7 @@ export declare class DevPortalAuthenticationProvider extends CoreAuthenticationP
27
29
  private setPreviewState;
28
30
  private ensureLiveBackend;
29
31
  refreshUserProfile(): Promise<boolean>;
32
+ private refreshUserProfileInternal;
30
33
  signIn(_: AuthActionContext, { redirectTo }?: AuthActionOptions): Promise<void>;
31
34
  signUp(_: AuthActionContext, { redirectTo }?: AuthActionOptions): Promise<void>;
32
35
  signOut(_: AuthActionContext): Promise<void>;
@@ -42,6 +42,7 @@ export declare const useAuthState: import("zustand").UseBoundStore<Omit<import("
42
42
  getOptions: () => Partial<import("zustand/middleware").PersistOptions<AuthState, unknown, unknown>>;
43
43
  };
44
44
  }>;
45
+ export declare const waitForAuthStateHydration: () => Promise<void>;
45
46
  export interface UserProfile {
46
47
  sub: string;
47
48
  email: string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mehdad67/apitogo",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -7,7 +7,11 @@ import type {
7
7
  AuthenticationProviderInitializer,
8
8
  } from "../authentication.js";
9
9
  import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
10
- import { type UserProfile, useAuthState } from "../state.js";
10
+ import {
11
+ type UserProfile,
12
+ useAuthState,
13
+ waitForAuthStateHydration,
14
+ } from "../state.js";
11
15
  import type {
12
16
  DevPortalAuthMode,
13
17
  EndUserMeResponse,
@@ -43,6 +47,8 @@ export class DevPortalAuthenticationProvider
43
47
  private readonly redirectToAfterSignOut: string;
44
48
  private authMode: DevPortalAuthMode = "preview";
45
49
  private backendAvailable = false;
50
+ private refreshGeneration = 0;
51
+ private refreshInFlight: Promise<boolean> | null = null;
46
52
 
47
53
  constructor({
48
54
  apiBaseUrl,
@@ -58,6 +64,12 @@ export class DevPortalAuthenticationProvider
58
64
  }
59
65
 
60
66
  async initialize(_context: ZudokuContext): Promise<void> {
67
+ // Node SSR cannot reach local HTTPS backends with self-signed certs.
68
+ if (typeof window === "undefined") {
69
+ return;
70
+ }
71
+
72
+ await waitForAuthStateHydration();
61
73
  await this.syncBackendAvailability();
62
74
  if (!this.backendAvailable) {
63
75
  this.setPreviewState();
@@ -68,6 +80,12 @@ export class DevPortalAuthenticationProvider
68
80
  }
69
81
 
70
82
  onPageLoad = async () => {
83
+ if (typeof window === "undefined") {
84
+ return;
85
+ }
86
+
87
+ await waitForAuthStateHydration();
88
+ await this.syncBackendAvailability();
71
89
  if (!this.backendAvailable) {
72
90
  this.setPreviewState();
73
91
  return;
@@ -113,6 +131,18 @@ export class DevPortalAuthenticationProvider
113
131
  }
114
132
 
115
133
  async refreshUserProfile(): Promise<boolean> {
134
+ if (this.refreshInFlight) {
135
+ return this.refreshInFlight;
136
+ }
137
+
138
+ this.refreshInFlight = this.refreshUserProfileInternal().finally(() => {
139
+ this.refreshInFlight = null;
140
+ });
141
+
142
+ return this.refreshInFlight;
143
+ }
144
+
145
+ private async refreshUserProfileInternal(): Promise<boolean> {
116
146
  if (!this.apiBaseUrl) {
117
147
  this.setPreviewState();
118
148
  return false;
@@ -123,20 +153,34 @@ export class DevPortalAuthenticationProvider
123
153
  return false;
124
154
  }
125
155
 
156
+ await waitForAuthStateHydration();
126
157
  await this.syncBackendAvailability();
127
158
  if (!this.backendAvailable) {
128
159
  this.setPreviewState();
129
160
  return false;
130
161
  }
131
162
 
163
+ const generation = ++this.refreshGeneration;
164
+ useAuthState.setState({ isPending: true });
165
+
132
166
  try {
133
167
  const response = await fetch(`${this.apiBaseUrl}/api/v1/auth/me`, {
134
168
  method: "GET",
135
169
  headers: { Accept: "application/json" },
136
170
  credentials: "include",
171
+ redirect: "manual",
137
172
  });
138
173
 
139
- if (response.status === 401) {
174
+ if (generation !== this.refreshGeneration) {
175
+ return useAuthState.getState().isAuthenticated;
176
+ }
177
+
178
+ // Backends may still respond with 302 to the IdP; treat as unauthenticated.
179
+ if (
180
+ response.type === "opaqueredirect" ||
181
+ response.status === 302 ||
182
+ response.status === 401
183
+ ) {
140
184
  useAuthState.setState({
141
185
  isAuthenticated: false,
142
186
  isPending: false,
@@ -158,22 +202,30 @@ export class DevPortalAuthenticationProvider
158
202
  }
159
203
 
160
204
  const me = (await response.json()) as EndUserMeResponse;
205
+ if (generation !== this.refreshGeneration) {
206
+ return useAuthState.getState().isAuthenticated;
207
+ }
208
+
161
209
  const profile: UserProfile = mapEndUserMeToProfile(me);
162
210
 
163
- useAuthState.getState().setLoggedIn({
211
+ useAuthState.setState({
212
+ isAuthenticated: true,
213
+ isPending: false,
164
214
  profile,
165
215
  providerData: {
166
216
  type: "dev-portal",
167
217
  apiBaseUrl: this.apiBaseUrl,
168
218
  authMode: "live",
169
219
  },
170
- });
171
- useAuthState.setState({
172
220
  isBackendAvailable: true,
173
221
  authMode: "live",
174
222
  });
175
223
  return true;
176
224
  } catch {
225
+ if (generation !== this.refreshGeneration) {
226
+ return useAuthState.getState().isAuthenticated;
227
+ }
228
+
177
229
  this.setPreviewState();
178
230
  return false;
179
231
  }
@@ -77,12 +77,25 @@ export const authState = create<AuthState>()(
77
77
  }),
78
78
  {
79
79
  merge: (persistedState, currentState) => {
80
+ const persisted =
81
+ typeof persistedState === "object" && persistedState !== null
82
+ ? (persistedState as Partial<AuthState>)
83
+ : {};
84
+
85
+ // Cookie/session auth is refreshed from the backend on load. Do not
86
+ // restore identity fields from localStorage or OIDC return looks logged out.
80
87
  return {
81
88
  ...currentState,
82
89
  isPending: false,
83
- ...(typeof persistedState === "object" ? persistedState : {}),
90
+ isBackendAvailable:
91
+ persisted.isBackendAvailable ?? currentState.isBackendAvailable,
92
+ authMode: persisted.authMode ?? currentState.authMode,
84
93
  };
85
94
  },
95
+ partialize: (state) => ({
96
+ isBackendAvailable: state.isBackendAvailable,
97
+ authMode: state.authMode,
98
+ }),
86
99
  name: "auth-state",
87
100
  storage: createJSONStorage(() => localStorage),
88
101
  },
@@ -93,6 +106,25 @@ syncZustandState(authState);
93
106
 
94
107
  export const useAuthState = authState;
95
108
 
109
+ /** Wait until persisted auth metadata has rehydrated from localStorage. */
110
+ export const waitForAuthStateHydration = (): Promise<void> => {
111
+ if (typeof window === "undefined") {
112
+ return Promise.resolve();
113
+ }
114
+
115
+ const { persist } = useAuthState;
116
+ if (persist.hasHydrated()) {
117
+ return Promise.resolve();
118
+ }
119
+
120
+ return new Promise((resolve) => {
121
+ const unsubscribe = persist.onFinishHydration(() => {
122
+ unsubscribe();
123
+ resolve();
124
+ });
125
+ });
126
+ };
127
+
96
128
  export interface UserProfile {
97
129
  sub: string;
98
130
  email: string | undefined;
@@ -73,13 +73,16 @@ const ProfileMenu = () => {
73
73
  const context = useZudoku();
74
74
  const profileItems = context.getProfileMenuItems();
75
75
  const auth = useAuth();
76
- const { isAuthEnabled, isAuthenticated, profile, isBackendAvailable } = auth;
76
+ const { isAuthEnabled, isAuthenticated, isPending, profile, isBackendAvailable } =
77
+ auth;
77
78
 
78
79
  if (!isAuthEnabled || !isBackendAvailable) return null;
79
80
 
80
81
  return (
81
82
  <ClientOnly fallback={<Skeleton className="rounded-sm h-5 w-24 mr-4" />}>
82
- {!isAuthenticated ? (
83
+ {isPending ? (
84
+ <Skeleton className="rounded-sm h-9 w-24" />
85
+ ) : !isAuthenticated ? (
83
86
  <Button size="lg" variant="ghost" onClick={() => auth.login()}>
84
87
  Login
85
88
  </Button>
@@ -130,7 +130,7 @@ export const MobileTopNavigation = () => {
130
130
  getProfileMenuItems,
131
131
  } = context;
132
132
  const headerNavigation = header?.navigation ?? [];
133
- const { isAuthenticated, profile, isAuthEnabled, isBackendAvailable } =
133
+ const { isAuthenticated, profile, isAuthEnabled, isBackendAvailable, isPending } =
134
134
  authState;
135
135
  const [drawerOpen, setDrawerOpen] = useState(false);
136
136
 
@@ -237,7 +237,9 @@ export const MobileTopNavigation = () => {
237
237
  <ClientOnly
238
238
  fallback={<Skeleton className="rounded-sm h-8 w-16" />}
239
239
  >
240
- {isAuthenticated ? (
240
+ {isPending ? (
241
+ <Skeleton className="rounded-sm h-8 w-16" />
242
+ ) : isAuthenticated ? (
241
243
  <Button asChild variant="outline">
242
244
  <Link
243
245
  to="/signout"