@lukoweb/apitogo 0.2.31 → 0.2.34

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 (41) hide show
  1. package/dist/cli/cli.js +436 -153
  2. package/dist/declarations/app/entry.server.d.ts +2 -2
  3. package/dist/declarations/app/main.d.ts +3 -1
  4. package/dist/declarations/cli/platform/client/platform-api.d.ts +0 -4
  5. package/dist/declarations/cli/platform/helpers/deploy-artifact-hash.d.ts +1 -1
  6. package/dist/declarations/cli/platform/helpers/package-files.d.ts +1 -1
  7. package/dist/declarations/cli/platform/helpers/zip.d.ts +1 -1
  8. package/dist/declarations/cli/platform/tools/infrastructure.d.ts +0 -4
  9. package/dist/declarations/config/effective-redirects.d.ts +6 -0
  10. package/dist/declarations/config/header-navigation-guard.d.ts +2 -0
  11. package/dist/declarations/config/resolve-modules.d.ts +1 -4
  12. package/dist/declarations/config/validators/HeaderNavigationSchema.d.ts +1 -2
  13. package/docs/cli/commands.md +19 -0
  14. package/docs/configuration/gateway.md +52 -0
  15. package/docs/guides/customer-account.md +11 -1
  16. package/docs/mcp/tools.md +101 -102
  17. package/package.json +2 -1
  18. package/src/app/demo.tsx +78 -77
  19. package/src/app/entry.client.tsx +115 -107
  20. package/src/app/entry.server.tsx +164 -160
  21. package/src/app/main.tsx +175 -167
  22. package/src/app/standalone.tsx +61 -60
  23. package/src/config/effective-redirects.ts +31 -0
  24. package/src/config/header-navigation-guard.ts +13 -0
  25. package/src/config/resolve-modules.ts +331 -354
  26. package/src/config/v2/home-redirect.ts +104 -98
  27. package/src/config/v2/index.ts +127 -126
  28. package/src/config/v2/ledger.ts +242 -235
  29. package/src/config/v2/resolve.ts +12 -4
  30. package/src/config/v2/rules.ts +716 -714
  31. package/src/config/v2/services.ts +799 -663
  32. package/src/config/v2/workspace.ts +17 -1
  33. package/src/config/validators/HeaderNavigationSchema.ts +42 -44
  34. package/src/lib/assets/powered-by-icon.png +0 -0
  35. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +667 -602
  36. package/src/lib/authentication/providers/dev-portal.tsx +476 -448
  37. package/src/lib/components/HeaderNavigation.tsx +176 -176
  38. package/src/lib/components/MobileTopNavigation.tsx +283 -283
  39. package/src/lib/components/navigation/PoweredByZudoku.tsx +42 -39
  40. package/src/vite/plugin-offroot-resolve.ts +68 -67
  41. package/src/vite/prerender/prerender.ts +242 -242
@@ -1,448 +1,476 @@
1
- import type { DevPortalAuthenticationConfig } from "../../../config/config.js";
2
- import type { ZudokuContext } from "../../core/ZudokuContext.js";
3
- import type {
4
- AuthActionContext,
5
- AuthActionOptions,
6
- AuthenticationPlugin,
7
- AuthenticationProviderInitializer,
8
- } from "../authentication.js";
9
- import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
10
- import {
11
- type UserProfile,
12
- useAuthState,
13
- waitForAuthStateHydration,
14
- } from "../state.js";
15
- import {
16
- DevPortalForgotPasswordPage,
17
- DevPortalLoginPage,
18
- DevPortalRegisterPage,
19
- DevPortalResetPasswordPage,
20
- DevPortalVerifyEmailPage,
21
- } from "./dev-portal-auth-pages.js";
22
- import type {
23
- DevPortalAuthMode,
24
- UserMeResponse,
25
- } from "./dev-portal-constants.js";
26
- import {
27
- clearDemoSession,
28
- readDemoSession,
29
- resolveDemoMode,
30
- activateDemoSession,
31
- } from "./dev-portal-demo.js";
32
- import {
33
- buildDevPortalLogoutUrl,
34
- isNativeAuthEnabled,
35
- isPlaceholderDevPortalApiUrl,
36
- mapUserMeToProfile,
37
- probeDevPortalBackend,
38
- resolveDevPortalApiBaseUrl,
39
- toAbsoluteReturnUrl,
40
- } from "./dev-portal-utils.js";
41
-
42
- export type DevPortalProviderData = {
43
- type: "dev-portal";
44
- apiBaseUrl: string;
45
- authMode: DevPortalAuthMode;
46
- isDemoSession?: boolean;
47
- demoMode?: boolean;
48
- };
49
-
50
- declare module "../state.js" {
51
- interface ProviderDataRegistry {
52
- "dev-portal": DevPortalProviderData;
53
- }
54
- }
55
-
56
- export class DevPortalAuthenticationProvider
57
- extends CoreAuthenticationPlugin
58
- implements AuthenticationPlugin
59
- {
60
- private readonly apiBaseUrl: string | undefined;
61
- private readonly demoMode: boolean;
62
- private readonly redirectToAfterSignIn: string | undefined;
63
- private readonly redirectToAfterSignUp: string | undefined;
64
- private readonly redirectToAfterSignOut: string;
65
- private backendAvailable = false;
66
- private refreshGeneration = 0;
67
- private refreshInFlight: Promise<boolean> | null = null;
68
-
69
- private authConfigNativeEnabled: boolean | null = null;
70
-
71
- constructor({
72
- apiBaseUrl,
73
- demoMode,
74
- redirectToAfterSignIn,
75
- redirectToAfterSignUp,
76
- redirectToAfterSignOut = "/",
77
- }: DevPortalAuthenticationConfig) {
78
- super();
79
- this.apiBaseUrl = resolveDevPortalApiBaseUrl({ apiBaseUrl });
80
- this.demoMode = resolveDemoMode({ demoMode });
81
- this.redirectToAfterSignIn = redirectToAfterSignIn;
82
- this.redirectToAfterSignUp = redirectToAfterSignUp;
83
- this.redirectToAfterSignOut = redirectToAfterSignOut;
84
- }
85
-
86
- getRoutes() {
87
- const defaultReturnUrl =
88
- this.redirectToAfterSignIn ?? this.redirectToAfterSignUp ?? "/";
89
- const pageProps = {
90
- apiBaseUrl: this.apiBaseUrl,
91
- demoMode: this.demoMode,
92
- };
93
- return [
94
- ...super.getRoutes(),
95
- {
96
- path: "/login",
97
- element: (
98
- <DevPortalLoginPage
99
- {...pageProps}
100
- defaultReturnUrl={defaultReturnUrl}
101
- />
102
- ),
103
- },
104
- {
105
- path: "/register",
106
- element: (
107
- <DevPortalRegisterPage
108
- apiBaseUrl={this.apiBaseUrl}
109
- defaultReturnUrl={
110
- this.redirectToAfterSignUp ?? this.redirectToAfterSignIn ?? "/"
111
- }
112
- />
113
- ),
114
- },
115
- {
116
- path: "/verify-email",
117
- element: <DevPortalVerifyEmailPage {...pageProps} />,
118
- },
119
- {
120
- path: "/forgot-password",
121
- element: <DevPortalForgotPasswordPage {...pageProps} />,
122
- },
123
- {
124
- path: "/reset-password",
125
- element: <DevPortalResetPasswordPage {...pageProps} />,
126
- },
127
- ];
128
- }
129
-
130
- async initialize(_context: ZudokuContext): Promise<void> {
131
- // Node SSR cannot reach local HTTPS backends with self-signed certs.
132
- if (typeof window === "undefined") {
133
- return;
134
- }
135
-
136
- await waitForAuthStateHydration();
137
- await this.syncBackendAvailability();
138
- if (this.backendAvailable && this.apiBaseUrl) {
139
- try {
140
- const { fetchDevPortalAuthConfig } =
141
- await import("./dev-portal-utils.js");
142
- const config = await fetchDevPortalAuthConfig(this.apiBaseUrl);
143
- this.authConfigNativeEnabled = isNativeAuthEnabled(config);
144
- } catch {
145
- this.authConfigNativeEnabled = null;
146
- }
147
- }
148
- if (!this.backendAvailable) {
149
- if (this.demoMode && readDemoSession()) {
150
- this.setDemoAuthenticatedState();
151
- return;
152
- }
153
-
154
- this.setPreviewState();
155
- return;
156
- }
157
-
158
- await this.refreshUserProfile();
159
- }
160
-
161
- onPageLoad = async () => {
162
- if (typeof window === "undefined") {
163
- return;
164
- }
165
-
166
- await waitForAuthStateHydration();
167
- await this.syncBackendAvailability();
168
- if (!this.backendAvailable) {
169
- if (this.demoMode && readDemoSession()) {
170
- this.setDemoAuthenticatedState();
171
- return;
172
- }
173
-
174
- this.setPreviewState();
175
- return;
176
- }
177
-
178
- await this.refreshUserProfile();
179
- };
180
-
181
- private async syncBackendAvailability(): Promise<void> {
182
- if (!this.apiBaseUrl || isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
183
- this.backendAvailable = false;
184
- return;
185
- }
186
-
187
- this.backendAvailable = await probeDevPortalBackend(this.apiBaseUrl);
188
- }
189
-
190
- private setPreviewState(): void {
191
- useAuthState.setState({
192
- isAuthenticated: false,
193
- isPending: false,
194
- sessionChecked: true,
195
- profile: null,
196
- providerData: this.apiBaseUrl
197
- ? {
198
- type: "dev-portal",
199
- apiBaseUrl: this.apiBaseUrl,
200
- authMode: "preview",
201
- demoMode: this.demoMode,
202
- }
203
- : this.demoMode
204
- ? {
205
- type: "dev-portal",
206
- apiBaseUrl: "",
207
- authMode: "preview",
208
- demoMode: true,
209
- }
210
- : null,
211
- isBackendAvailable: false,
212
- authMode: "preview",
213
- });
214
- }
215
-
216
- private setLiveUnauthenticatedState(): void {
217
- if (!this.apiBaseUrl) {
218
- this.setPreviewState();
219
- return;
220
- }
221
-
222
- useAuthState.setState({
223
- isAuthenticated: false,
224
- isPending: false,
225
- sessionChecked: true,
226
- profile: null,
227
- providerData: {
228
- type: "dev-portal",
229
- apiBaseUrl: this.apiBaseUrl,
230
- authMode: "live",
231
- demoMode: this.demoMode,
232
- },
233
- isBackendAvailable: true,
234
- authMode: "live",
235
- });
236
- }
237
-
238
- private setDemoAuthenticatedState(): void {
239
- activateDemoSession(this.apiBaseUrl);
240
- }
241
-
242
- private ensureConfiguredBackend(): string {
243
- if (!this.apiBaseUrl || isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
244
- if (this.demoMode) {
245
- return "";
246
- }
247
-
248
- throw new Error("Dev portal authentication is not available in preview.");
249
- }
250
-
251
- return this.apiBaseUrl;
252
- }
253
-
254
- async refreshUserProfile(): Promise<boolean> {
255
- if (this.refreshInFlight) {
256
- return this.refreshInFlight;
257
- }
258
-
259
- this.refreshInFlight = this.refreshUserProfileInternal().finally(() => {
260
- this.refreshInFlight = null;
261
- });
262
-
263
- return this.refreshInFlight;
264
- }
265
-
266
- private async refreshUserProfileInternal(): Promise<boolean> {
267
- if (this.demoMode && readDemoSession() && !this.backendAvailable) {
268
- this.setDemoAuthenticatedState();
269
- return true;
270
- }
271
-
272
- if (!this.apiBaseUrl) {
273
- this.setPreviewState();
274
- return false;
275
- }
276
-
277
- if (isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
278
- if (this.demoMode && readDemoSession()) {
279
- this.setDemoAuthenticatedState();
280
- return true;
281
- }
282
-
283
- this.setPreviewState();
284
- return false;
285
- }
286
-
287
- await waitForAuthStateHydration();
288
- await this.syncBackendAvailability();
289
- if (!this.backendAvailable) {
290
- if (this.demoMode && readDemoSession()) {
291
- this.setDemoAuthenticatedState();
292
- return true;
293
- }
294
-
295
- this.setPreviewState();
296
- return false;
297
- }
298
-
299
- const generation = ++this.refreshGeneration;
300
- useAuthState.setState({ isPending: true });
301
-
302
- try {
303
- const response = await fetch(`${this.apiBaseUrl}/api/v1/auth/me`, {
304
- method: "GET",
305
- headers: { Accept: "application/json" },
306
- credentials: "include",
307
- redirect: "manual",
308
- });
309
-
310
- if (generation !== this.refreshGeneration) {
311
- return useAuthState.getState().isAuthenticated;
312
- }
313
-
314
- // Backends may still respond with 302 to the IdP; treat as unauthenticated.
315
- if (
316
- response.type === "opaqueredirect" ||
317
- response.status === 302 ||
318
- response.status === 401
319
- ) {
320
- this.setLiveUnauthenticatedState();
321
- return false;
322
- }
323
-
324
- if (!response.ok) {
325
- this.setLiveUnauthenticatedState();
326
- return false;
327
- }
328
-
329
- const me = (await response.json()) as UserMeResponse;
330
- if (generation !== this.refreshGeneration) {
331
- return useAuthState.getState().isAuthenticated;
332
- }
333
-
334
- const profile: UserProfile = mapUserMeToProfile(me);
335
-
336
- useAuthState.setState({
337
- isAuthenticated: true,
338
- isPending: false,
339
- sessionChecked: true,
340
- profile,
341
- providerData: {
342
- type: "dev-portal",
343
- apiBaseUrl: this.apiBaseUrl,
344
- authMode: "live",
345
- demoMode: this.demoMode,
346
- },
347
- isBackendAvailable: true,
348
- authMode: "live",
349
- });
350
- return true;
351
- } catch {
352
- if (generation !== this.refreshGeneration) {
353
- return useAuthState.getState().isAuthenticated;
354
- }
355
-
356
- if (this.backendAvailable) {
357
- this.setLiveUnauthenticatedState();
358
- return false;
359
- }
360
-
361
- this.setPreviewState();
362
- return false;
363
- }
364
- }
365
-
366
- async signIn(
367
- _: AuthActionContext,
368
- { redirectTo }: AuthActionOptions = {},
369
- ): Promise<void> {
370
- if (!this.backendAvailable && this.demoMode) {
371
- const target =
372
- redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
373
- const url = new URL("/login", window.location.origin);
374
- url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
375
- window.location.assign(url.toString());
376
- return;
377
- }
378
-
379
- this.ensureConfiguredBackend();
380
- const target =
381
- redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
382
- const url = new URL("/login", window.location.origin);
383
- url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
384
- window.location.assign(url.toString());
385
- }
386
-
387
- async signUp(
388
- _: AuthActionContext,
389
- { redirectTo }: AuthActionOptions = {},
390
- ): Promise<void> {
391
- if (!this.backendAvailable && this.demoMode) {
392
- const target =
393
- redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
394
- const url = new URL("/register", window.location.origin);
395
- url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
396
- window.location.assign(url.toString());
397
- return;
398
- }
399
-
400
- this.ensureConfiguredBackend();
401
- const target =
402
- redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
403
- const url = new URL("/register", window.location.origin);
404
- url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
405
- window.location.assign(url.toString());
406
- }
407
-
408
- async signOut(_: AuthActionContext): Promise<void> {
409
- const providerData = useAuthState.getState().providerData as
410
- | DevPortalProviderData
411
- | null
412
- | undefined;
413
-
414
- if (providerData?.isDemoSession || (this.demoMode && readDemoSession())) {
415
- clearDemoSession();
416
- this.setPreviewState();
417
- return;
418
- }
419
-
420
- const apiBaseUrl = this.ensureConfiguredBackend();
421
- window.location.assign(
422
- buildDevPortalLogoutUrl(apiBaseUrl, this.redirectToAfterSignOut),
423
- );
424
- }
425
-
426
- hasDistinctSignUpFlow(): boolean {
427
- if (this.demoMode && !this.backendAvailable) {
428
- return true;
429
- }
430
-
431
- return this.authConfigNativeEnabled !== false;
432
- }
433
-
434
- async signRequest(request: Request): Promise<Request> {
435
- return request;
436
- }
437
-
438
- /** Called from the login page after successful demo credential check. */
439
- completeDemoLogin(): void {
440
- activateDemoSession(this.apiBaseUrl);
441
- }
442
- }
443
-
444
- const devPortalAuth: AuthenticationProviderInitializer<
445
- DevPortalAuthenticationConfig
446
- > = (options) => new DevPortalAuthenticationProvider(options);
447
-
448
- export default devPortalAuth;
1
+ import type { DevPortalAuthenticationConfig } from "../../../config/config.js";
2
+ import type { ZudokuContext } from "../../core/ZudokuContext.js";
3
+ import type {
4
+ AuthActionContext,
5
+ AuthActionOptions,
6
+ AuthenticationPlugin,
7
+ AuthenticationProviderInitializer,
8
+ } from "../authentication.js";
9
+ import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
10
+ import {
11
+ type UserProfile,
12
+ useAuthState,
13
+ waitForAuthStateHydration,
14
+ } from "../state.js";
15
+ import {
16
+ DevPortalForgotPasswordPage,
17
+ DevPortalLoginPage,
18
+ DevPortalRegisterPage,
19
+ DevPortalResetPasswordPage,
20
+ DevPortalVerifyEmailPage,
21
+ } from "./dev-portal-auth-pages.js";
22
+ import type {
23
+ DevPortalAuthMode,
24
+ UserMeResponse,
25
+ } from "./dev-portal-constants.js";
26
+ import {
27
+ clearDemoSession,
28
+ readDemoSession,
29
+ resolveDemoMode,
30
+ activateDemoSession,
31
+ } from "./dev-portal-demo.js";
32
+ import {
33
+ buildDevPortalLogoutUrl,
34
+ isNativeAuthEnabled,
35
+ isPlaceholderDevPortalApiUrl,
36
+ mapUserMeToProfile,
37
+ probeDevPortalBackend,
38
+ resolveDevPortalApiBaseUrl,
39
+ toAbsoluteReturnUrl,
40
+ } from "./dev-portal-utils.js";
41
+
42
+ export type DevPortalProviderData = {
43
+ type: "dev-portal";
44
+ apiBaseUrl: string;
45
+ authMode: DevPortalAuthMode;
46
+ isDemoSession?: boolean;
47
+ demoMode?: boolean;
48
+ };
49
+
50
+ declare module "../state.js" {
51
+ interface ProviderDataRegistry {
52
+ "dev-portal": DevPortalProviderData;
53
+ }
54
+ }
55
+
56
+ export class DevPortalAuthenticationProvider
57
+ extends CoreAuthenticationPlugin
58
+ implements AuthenticationPlugin
59
+ {
60
+ private readonly apiBaseUrl: string | undefined;
61
+ private readonly demoMode: boolean;
62
+ private readonly redirectToAfterSignIn: string | undefined;
63
+ private readonly redirectToAfterSignUp: string | undefined;
64
+ private readonly redirectToAfterSignOut: string;
65
+ private backendAvailable = false;
66
+ private refreshGeneration = 0;
67
+ private refreshInFlight: Promise<boolean> | null = null;
68
+
69
+ private authConfigNativeEnabled: boolean | null = null;
70
+
71
+ constructor({
72
+ apiBaseUrl,
73
+ demoMode,
74
+ redirectToAfterSignIn,
75
+ redirectToAfterSignUp,
76
+ redirectToAfterSignOut = "/",
77
+ }: DevPortalAuthenticationConfig) {
78
+ super();
79
+ this.apiBaseUrl = resolveDevPortalApiBaseUrl({ apiBaseUrl });
80
+ this.demoMode = resolveDemoMode({ demoMode });
81
+ this.redirectToAfterSignIn = redirectToAfterSignIn;
82
+ this.redirectToAfterSignUp = redirectToAfterSignUp;
83
+ this.redirectToAfterSignOut = redirectToAfterSignOut;
84
+ }
85
+
86
+ getRoutes() {
87
+ const defaultReturnUrl =
88
+ this.redirectToAfterSignIn ?? this.redirectToAfterSignUp ?? "/";
89
+ const pageProps = {
90
+ apiBaseUrl: this.apiBaseUrl,
91
+ demoMode: this.demoMode,
92
+ };
93
+ return [
94
+ ...super.getRoutes(),
95
+ {
96
+ path: "/login",
97
+ element: (
98
+ <DevPortalLoginPage
99
+ {...pageProps}
100
+ defaultReturnUrl={defaultReturnUrl}
101
+ />
102
+ ),
103
+ },
104
+ {
105
+ path: "/register",
106
+ element: (
107
+ <DevPortalRegisterPage
108
+ apiBaseUrl={this.apiBaseUrl}
109
+ defaultReturnUrl={
110
+ this.redirectToAfterSignUp ?? this.redirectToAfterSignIn ?? "/"
111
+ }
112
+ />
113
+ ),
114
+ },
115
+ {
116
+ path: "/verify-email",
117
+ element: <DevPortalVerifyEmailPage {...pageProps} />,
118
+ },
119
+ {
120
+ path: "/forgot-password",
121
+ element: <DevPortalForgotPasswordPage {...pageProps} />,
122
+ },
123
+ {
124
+ path: "/reset-password",
125
+ element: <DevPortalResetPasswordPage {...pageProps} />,
126
+ },
127
+ ];
128
+ }
129
+
130
+ async initialize(_context: ZudokuContext): Promise<void> {
131
+ // Node SSR cannot reach local HTTPS backends with self-signed certs.
132
+ if (typeof window === "undefined") {
133
+ return;
134
+ }
135
+
136
+ await waitForAuthStateHydration();
137
+ await this.syncBackendAvailability();
138
+ if (this.backendAvailable && this.apiBaseUrl) {
139
+ try {
140
+ const { fetchDevPortalAuthConfig } =
141
+ await import("./dev-portal-utils.js");
142
+ const config = await fetchDevPortalAuthConfig(this.apiBaseUrl);
143
+ this.authConfigNativeEnabled = isNativeAuthEnabled(config);
144
+ } catch {
145
+ this.authConfigNativeEnabled = null;
146
+ }
147
+ }
148
+ if (!this.backendAvailable) {
149
+ if (this.demoMode && readDemoSession()) {
150
+ this.setDemoAuthenticatedState();
151
+ return;
152
+ }
153
+
154
+ this.setPreviewState();
155
+ return;
156
+ }
157
+
158
+ await this.refreshUserProfile();
159
+ }
160
+
161
+ onPageLoad = async () => {
162
+ if (typeof window === "undefined") {
163
+ return;
164
+ }
165
+
166
+ await waitForAuthStateHydration();
167
+ await this.syncBackendAvailability();
168
+ if (!this.backendAvailable) {
169
+ if (this.demoMode && readDemoSession()) {
170
+ this.setDemoAuthenticatedState();
171
+ return;
172
+ }
173
+
174
+ this.setPreviewState();
175
+ return;
176
+ }
177
+
178
+ await this.refreshUserProfile();
179
+ };
180
+
181
+ private async syncBackendAvailability(): Promise<void> {
182
+ if (!this.apiBaseUrl || isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
183
+ this.backendAvailable = false;
184
+ return;
185
+ }
186
+
187
+ this.backendAvailable = await probeDevPortalBackend(this.apiBaseUrl);
188
+ }
189
+
190
+ private setPreviewState(): void {
191
+ useAuthState.setState({
192
+ isAuthenticated: false,
193
+ isPending: false,
194
+ sessionChecked: true,
195
+ profile: null,
196
+ providerData: this.apiBaseUrl
197
+ ? {
198
+ type: "dev-portal",
199
+ apiBaseUrl: this.apiBaseUrl,
200
+ authMode: "preview",
201
+ demoMode: this.demoMode,
202
+ }
203
+ : this.demoMode
204
+ ? {
205
+ type: "dev-portal",
206
+ apiBaseUrl: "",
207
+ authMode: "preview",
208
+ demoMode: true,
209
+ }
210
+ : null,
211
+ isBackendAvailable: false,
212
+ authMode: "preview",
213
+ });
214
+ }
215
+
216
+ private setLiveUnauthenticatedState(): void {
217
+ if (!this.apiBaseUrl) {
218
+ this.setPreviewState();
219
+ return;
220
+ }
221
+
222
+ useAuthState.setState({
223
+ isAuthenticated: false,
224
+ isPending: false,
225
+ sessionChecked: true,
226
+ profile: null,
227
+ providerData: {
228
+ type: "dev-portal",
229
+ apiBaseUrl: this.apiBaseUrl,
230
+ authMode: "live",
231
+ demoMode: this.demoMode,
232
+ },
233
+ isBackendAvailable: true,
234
+ authMode: "live",
235
+ });
236
+ }
237
+
238
+ private setDemoAuthenticatedState(): void {
239
+ activateDemoSession(this.apiBaseUrl);
240
+ }
241
+
242
+ private ensureConfiguredBackend(): string {
243
+ if (!this.apiBaseUrl || isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
244
+ if (this.demoMode) {
245
+ return "";
246
+ }
247
+
248
+ throw new Error("Dev portal authentication is not available in preview.");
249
+ }
250
+
251
+ return this.apiBaseUrl;
252
+ }
253
+
254
+ async refreshUserProfile(): Promise<boolean> {
255
+ if (this.refreshInFlight) {
256
+ return this.refreshInFlight;
257
+ }
258
+
259
+ this.refreshInFlight = this.refreshUserProfileInternal().finally(() => {
260
+ this.refreshInFlight = null;
261
+ });
262
+
263
+ return this.refreshInFlight;
264
+ }
265
+
266
+ private async refreshUserProfileInternal(): Promise<boolean> {
267
+ if (this.demoMode && readDemoSession() && !this.backendAvailable) {
268
+ this.setDemoAuthenticatedState();
269
+ return true;
270
+ }
271
+
272
+ if (!this.apiBaseUrl) {
273
+ this.setPreviewState();
274
+ return false;
275
+ }
276
+
277
+ if (isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
278
+ if (this.demoMode && readDemoSession()) {
279
+ this.setDemoAuthenticatedState();
280
+ return true;
281
+ }
282
+
283
+ this.setPreviewState();
284
+ return false;
285
+ }
286
+
287
+ await waitForAuthStateHydration();
288
+ await this.syncBackendAvailability();
289
+ if (!this.backendAvailable) {
290
+ if (this.demoMode && readDemoSession()) {
291
+ this.setDemoAuthenticatedState();
292
+ return true;
293
+ }
294
+
295
+ this.setPreviewState();
296
+ return false;
297
+ }
298
+
299
+ const generation = ++this.refreshGeneration;
300
+ useAuthState.setState({ isPending: true });
301
+
302
+ try {
303
+ // /auth/session answers 200 for everyone, so a visitor does not put a failed request in the console. A backend
304
+ // from before it existed answers 404: ask /auth/me there, which answers 401 for a visitor.
305
+ const requestInit: RequestInit = {
306
+ method: "GET",
307
+ headers: { Accept: "application/json" },
308
+ credentials: "include",
309
+ redirect: "manual",
310
+ };
311
+ let response = await fetch(
312
+ `${this.apiBaseUrl}/api/v1/auth/session`,
313
+ requestInit,
314
+ );
315
+ const hasSession = response.status !== 404;
316
+ if (!hasSession) {
317
+ response = await fetch(
318
+ `${this.apiBaseUrl}/api/v1/auth/me`,
319
+ requestInit,
320
+ );
321
+ }
322
+
323
+ if (generation !== this.refreshGeneration) {
324
+ return useAuthState.getState().isAuthenticated;
325
+ }
326
+
327
+ // Backends may still respond with 302 to the IdP; treat as unauthenticated.
328
+ if (
329
+ response.type === "opaqueredirect" ||
330
+ response.status === 302 ||
331
+ response.status === 401
332
+ ) {
333
+ this.setLiveUnauthenticatedState();
334
+ return false;
335
+ }
336
+
337
+ if (!response.ok) {
338
+ this.setLiveUnauthenticatedState();
339
+ return false;
340
+ }
341
+
342
+ let me: UserMeResponse;
343
+ if (hasSession) {
344
+ const session = (await response.json()) as {
345
+ authenticated?: boolean;
346
+ user?: UserMeResponse | null;
347
+ };
348
+ if (!session.authenticated || !session.user) {
349
+ if (generation === this.refreshGeneration) {
350
+ this.setLiveUnauthenticatedState();
351
+ }
352
+ return false;
353
+ }
354
+ me = session.user;
355
+ } else {
356
+ me = (await response.json()) as UserMeResponse;
357
+ }
358
+ if (generation !== this.refreshGeneration) {
359
+ return useAuthState.getState().isAuthenticated;
360
+ }
361
+
362
+ const profile: UserProfile = mapUserMeToProfile(me);
363
+
364
+ useAuthState.setState({
365
+ isAuthenticated: true,
366
+ isPending: false,
367
+ sessionChecked: true,
368
+ profile,
369
+ providerData: {
370
+ type: "dev-portal",
371
+ apiBaseUrl: this.apiBaseUrl,
372
+ authMode: "live",
373
+ demoMode: this.demoMode,
374
+ },
375
+ isBackendAvailable: true,
376
+ authMode: "live",
377
+ });
378
+ return true;
379
+ } catch {
380
+ if (generation !== this.refreshGeneration) {
381
+ return useAuthState.getState().isAuthenticated;
382
+ }
383
+
384
+ if (this.backendAvailable) {
385
+ this.setLiveUnauthenticatedState();
386
+ return false;
387
+ }
388
+
389
+ this.setPreviewState();
390
+ return false;
391
+ }
392
+ }
393
+
394
+ async signIn(
395
+ _: AuthActionContext,
396
+ { redirectTo }: AuthActionOptions = {},
397
+ ): Promise<void> {
398
+ if (!this.backendAvailable && this.demoMode) {
399
+ const target =
400
+ redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
401
+ const url = new URL("/login", window.location.origin);
402
+ url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
403
+ window.location.assign(url.toString());
404
+ return;
405
+ }
406
+
407
+ this.ensureConfiguredBackend();
408
+ const target =
409
+ redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
410
+ const url = new URL("/login", window.location.origin);
411
+ url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
412
+ window.location.assign(url.toString());
413
+ }
414
+
415
+ async signUp(
416
+ _: AuthActionContext,
417
+ { redirectTo }: AuthActionOptions = {},
418
+ ): Promise<void> {
419
+ if (!this.backendAvailable && this.demoMode) {
420
+ const target =
421
+ redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
422
+ const url = new URL("/register", window.location.origin);
423
+ url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
424
+ window.location.assign(url.toString());
425
+ return;
426
+ }
427
+
428
+ this.ensureConfiguredBackend();
429
+ const target =
430
+ redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
431
+ const url = new URL("/register", window.location.origin);
432
+ url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
433
+ window.location.assign(url.toString());
434
+ }
435
+
436
+ async signOut(_: AuthActionContext): Promise<void> {
437
+ const providerData = useAuthState.getState().providerData as
438
+ | DevPortalProviderData
439
+ | null
440
+ | undefined;
441
+
442
+ if (providerData?.isDemoSession || (this.demoMode && readDemoSession())) {
443
+ clearDemoSession();
444
+ this.setPreviewState();
445
+ return;
446
+ }
447
+
448
+ const apiBaseUrl = this.ensureConfiguredBackend();
449
+ window.location.assign(
450
+ buildDevPortalLogoutUrl(apiBaseUrl, this.redirectToAfterSignOut),
451
+ );
452
+ }
453
+
454
+ hasDistinctSignUpFlow(): boolean {
455
+ if (this.demoMode && !this.backendAvailable) {
456
+ return true;
457
+ }
458
+
459
+ return this.authConfigNativeEnabled !== false;
460
+ }
461
+
462
+ async signRequest(request: Request): Promise<Request> {
463
+ return request;
464
+ }
465
+
466
+ /** Called from the login page after successful demo credential check. */
467
+ completeDemoLogin(): void {
468
+ activateDemoSession(this.apiBaseUrl);
469
+ }
470
+ }
471
+
472
+ const devPortalAuth: AuthenticationProviderInitializer<
473
+ DevPortalAuthenticationConfig
474
+ > = (options) => new DevPortalAuthenticationProvider(options);
475
+
476
+ export default devPortalAuth;