@lukoweb/apitogo 0.1.53 → 0.1.55

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 (47) hide show
  1. package/dist/cli/cli.js +686 -373
  2. package/dist/declarations/config/apitogo-config-core.d.ts +14 -4
  3. package/dist/declarations/config/landing-manifest.d.ts +76 -0
  4. package/dist/declarations/config/local-manifest.d.ts +91 -1
  5. package/dist/declarations/config/validators/ModulesSchema.d.ts +227 -0
  6. package/dist/declarations/config/validators/ZudokuConfig.d.ts +84 -2
  7. package/dist/declarations/lib/authentication/header-nav-filter.d.ts +4 -0
  8. package/dist/declarations/lib/authentication/providers/dev-portal-auth-pages.d.ts +15 -0
  9. package/dist/declarations/lib/authentication/providers/dev-portal-constants.d.ts +19 -1
  10. package/dist/declarations/lib/authentication/providers/dev-portal-utils.d.ts +5 -1
  11. package/dist/declarations/lib/authentication/providers/dev-portal.d.ts +5 -0
  12. package/dist/declarations/lib/components/HeaderAuthActions.d.ts +3 -0
  13. package/dist/declarations/lib/components/SiteTitle.d.ts +6 -0
  14. package/dist/declarations/lib/components/ThemeSwitch.d.ts +3 -1
  15. package/dist/declarations/lib/components/TopNavigation.d.ts +1 -1
  16. package/dist/declarations/lib/core/ZudokuContext.d.ts +1 -0
  17. package/dist/flat-config.d.ts +61 -1
  18. package/package.json +1 -1
  19. package/src/app/main.css +2 -2
  20. package/src/config/apitogo-config-core.ts +140 -11
  21. package/src/config/apitogo-config-loader.browser.ts +17 -0
  22. package/src/config/apitogo-config-loader.node.ts +28 -8
  23. package/src/config/build-apitogo-config.ts +13 -4
  24. package/src/config/landing-manifest.ts +11 -0
  25. package/src/config/landing-openapi-showcase.ts +115 -0
  26. package/src/config/loader.ts +17 -3
  27. package/src/config/local-manifest.ts +146 -11
  28. package/src/config/resolve-modules.ts +22 -1
  29. package/src/config/validators/ModulesSchema.ts +84 -0
  30. package/src/config/validators/ZudokuConfig.ts +26 -1
  31. package/src/lib/authentication/auth-header-nav.ts +5 -15
  32. package/src/lib/authentication/header-nav-filter.ts +36 -0
  33. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +499 -0
  34. package/src/lib/authentication/providers/dev-portal-constants.ts +15 -1
  35. package/src/lib/authentication/providers/dev-portal-utils.ts +128 -3
  36. package/src/lib/authentication/providers/dev-portal.tsx +51 -6
  37. package/src/lib/components/Header.tsx +49 -39
  38. package/src/lib/components/HeaderAuthActions.tsx +36 -0
  39. package/src/lib/components/HeaderNavigation.tsx +11 -9
  40. package/src/lib/components/MobileTopNavigation.tsx +43 -24
  41. package/src/lib/components/SiteTitle.tsx +20 -0
  42. package/src/lib/components/ThemeSwitch.tsx +36 -2
  43. package/src/lib/components/TopNavigation.tsx +20 -33
  44. package/src/lib/core/ZudokuContext.ts +1 -0
  45. package/src/vite/config.ts +21 -0
  46. package/src/vite/plugin-auth.ts +4 -1
  47. package/src/vite/plugin-config.ts +39 -4
@@ -0,0 +1,499 @@
1
+ import { Spinner } from "@lukoweb/apitogo/components";
2
+ import { Button } from "@lukoweb/apitogo/ui/Button.js";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "@lukoweb/apitogo/ui/Card.js";
10
+ import { Input } from "@lukoweb/apitogo/ui/Input.js";
11
+ import { Label } from "@lukoweb/apitogo/ui/Label.js";
12
+ import { useCallback, useEffect, useState } from "react";
13
+ import { Link, useSearchParams } from "react-router";
14
+ import type { DevPortalAuthConfigResponse } from "./dev-portal-constants.js";
15
+ import {
16
+ buildDevPortalOidcChallengeUrl,
17
+ fetchDevPortalAuthConfig,
18
+ isNativeAuthEnabled,
19
+ postDevPortalAuth,
20
+ resolveDevPortalApiBaseUrl,
21
+ toAbsoluteReturnUrl,
22
+ } from "./dev-portal-utils.js";
23
+
24
+ type AuthPageShellProps = {
25
+ title: string;
26
+ description?: string;
27
+ children: React.ReactNode;
28
+ };
29
+
30
+ function AuthPageShell({ title, description, children }: AuthPageShellProps) {
31
+ return (
32
+ <div className="flex items-center justify-center mt-8 px-4">
33
+ <Card className="max-w-md w-full">
34
+ <CardHeader>
35
+ <CardTitle className="text-lg">{title}</CardTitle>
36
+ {description ? (
37
+ <CardDescription>{description}</CardDescription>
38
+ ) : null}
39
+ </CardHeader>
40
+ <CardContent className="space-y-4">{children}</CardContent>
41
+ </Card>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ function useReturnUrl(): string {
47
+ const [search] = useSearchParams();
48
+ return search.get("redirect") ?? search.get("returnUrl") ?? "/";
49
+ }
50
+
51
+ function useApiBaseUrl(apiBaseUrl?: string): string | undefined {
52
+ return resolveDevPortalApiBaseUrl({ apiBaseUrl });
53
+ }
54
+
55
+ function AuthError({ message }: { message: string | null }) {
56
+ if (!message) return null;
57
+ return <p className="text-sm text-destructive">{message}</p>;
58
+ }
59
+
60
+ function OidcProviderButtons({
61
+ apiBaseUrl,
62
+ config,
63
+ returnUrl,
64
+ }: {
65
+ apiBaseUrl: string;
66
+ config: DevPortalAuthConfigResponse;
67
+ returnUrl: string;
68
+ }) {
69
+ if (!config.providers.length) {
70
+ return null;
71
+ }
72
+
73
+ return (
74
+ <div className="space-y-2">
75
+ {isNativeAuthEnabled(config) ? (
76
+ <p className="text-sm text-muted-foreground text-center">
77
+ or continue with
78
+ </p>
79
+ ) : null}
80
+ {config.providers.map((provider) => (
81
+ <Button
82
+ key={provider.id}
83
+ type="button"
84
+ variant="outline"
85
+ className="w-full"
86
+ onClick={() => {
87
+ window.location.assign(
88
+ buildDevPortalOidcChallengeUrl(
89
+ apiBaseUrl,
90
+ provider.id,
91
+ returnUrl,
92
+ ),
93
+ );
94
+ }}
95
+ >
96
+ {provider.displayName || provider.id}
97
+ </Button>
98
+ ))}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
104
+ const baseUrl = useApiBaseUrl(apiBaseUrl);
105
+ const returnUrl = useReturnUrl();
106
+ const [config, setConfig] = useState<DevPortalAuthConfigResponse | null>(
107
+ null,
108
+ );
109
+ const [configLoading, setConfigLoading] = useState(true);
110
+ const [configError, setConfigError] = useState<string | null>(null);
111
+ const [email, setEmail] = useState("");
112
+ const [password, setPassword] = useState("");
113
+ const [error, setError] = useState<string | null>(null);
114
+ const [pending, setPending] = useState(false);
115
+
116
+ useEffect(() => {
117
+ if (!baseUrl) return;
118
+ setConfigLoading(true);
119
+ setConfigError(null);
120
+ void fetchDevPortalAuthConfig(baseUrl)
121
+ .then((loaded) => {
122
+ setConfig(loaded);
123
+ setConfigLoading(false);
124
+ })
125
+ .catch(() => {
126
+ setConfig(null);
127
+ setConfigLoading(false);
128
+ setConfigError(
129
+ `Could not reach the dev portal API at ${baseUrl}. If you use a local HTTPS certificate, open ${baseUrl}/api/v1/auth/status in your browser first to trust it.`,
130
+ );
131
+ });
132
+ }, [baseUrl]);
133
+
134
+ const onSubmit = useCallback(
135
+ async (event: React.FormEvent) => {
136
+ event.preventDefault();
137
+ if (!baseUrl) return;
138
+ setPending(true);
139
+ setError(null);
140
+ try {
141
+ await postDevPortalAuth(baseUrl, "/api/v1/auth/login", {
142
+ email,
143
+ password,
144
+ });
145
+ window.location.assign(toAbsoluteReturnUrl(returnUrl));
146
+ } catch (err) {
147
+ setError(err instanceof Error ? err.message : "Login failed.");
148
+ } finally {
149
+ setPending(false);
150
+ }
151
+ },
152
+ [baseUrl, email, password, returnUrl],
153
+ );
154
+
155
+ if (!baseUrl) {
156
+ return (
157
+ <AuthPageShell
158
+ title="Sign in"
159
+ description="Dev portal API is not configured."
160
+ >
161
+ <p className="text-sm text-muted-foreground">
162
+ Set authentication.apiBaseUrl to continue.
163
+ </p>
164
+ </AuthPageShell>
165
+ );
166
+ }
167
+
168
+ return (
169
+ <AuthPageShell title="Sign in" description="Access your developer account.">
170
+ {isNativeAuthEnabled(config) ? (
171
+ <form className="space-y-4" onSubmit={onSubmit}>
172
+ <div className="space-y-2">
173
+ <Label htmlFor="email">Email</Label>
174
+ <Input
175
+ id="email"
176
+ type="email"
177
+ autoComplete="email"
178
+ value={email}
179
+ onChange={(event) => setEmail(event.target.value)}
180
+ required
181
+ />
182
+ </div>
183
+ <div className="space-y-2">
184
+ <Label htmlFor="password">Password</Label>
185
+ <Input
186
+ id="password"
187
+ type="password"
188
+ autoComplete="current-password"
189
+ value={password}
190
+ onChange={(event) => setPassword(event.target.value)}
191
+ required
192
+ />
193
+ </div>
194
+ <AuthError message={error} />
195
+ <Button type="submit" className="w-full" disabled={pending}>
196
+ {pending ? <Spinner /> : "Sign in"}
197
+ </Button>
198
+ <div className="flex justify-between text-sm">
199
+ <Link
200
+ className="text-primary hover:underline"
201
+ to="/forgot-password"
202
+ >
203
+ Forgot password?
204
+ </Link>
205
+ <Link
206
+ className="text-primary hover:underline"
207
+ to={`/register?redirect=${encodeURIComponent(returnUrl)}`}
208
+ >
209
+ Create account
210
+ </Link>
211
+ </div>
212
+ </form>
213
+ ) : null}
214
+ {configError ? <AuthError message={configError} /> : null}
215
+ {configLoading ? (
216
+ <div className="flex justify-center">
217
+ <Spinner />
218
+ </div>
219
+ ) : config ? (
220
+ <>
221
+ <OidcProviderButtons
222
+ apiBaseUrl={baseUrl}
223
+ config={config}
224
+ returnUrl={returnUrl}
225
+ />
226
+ {!isNativeAuthEnabled(config) && !config.providers.length ? (
227
+ <p className="text-sm text-muted-foreground text-center">
228
+ No sign-in methods are configured.
229
+ </p>
230
+ ) : null}
231
+ </>
232
+ ) : null}
233
+ </AuthPageShell>
234
+ );
235
+ }
236
+
237
+ export function DevPortalRegisterPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
238
+ const baseUrl = useApiBaseUrl(apiBaseUrl);
239
+ const returnUrl = useReturnUrl();
240
+ const [email, setEmail] = useState("");
241
+ const [password, setPassword] = useState("");
242
+ const [displayName, setDisplayName] = useState("");
243
+ const [message, setMessage] = useState<string | null>(null);
244
+ const [error, setError] = useState<string | null>(null);
245
+ const [pending, setPending] = useState(false);
246
+
247
+ const onSubmit = useCallback(
248
+ async (event: React.FormEvent) => {
249
+ event.preventDefault();
250
+ if (!baseUrl) return;
251
+ setPending(true);
252
+ setError(null);
253
+ setMessage(null);
254
+ try {
255
+ const response = await postDevPortalAuth<{ message: string }>(
256
+ baseUrl,
257
+ "/api/v1/auth/register",
258
+ { email, password, displayName: displayName || null },
259
+ );
260
+ setMessage(response.message);
261
+ } catch (err) {
262
+ setError(err instanceof Error ? err.message : "Registration failed.");
263
+ } finally {
264
+ setPending(false);
265
+ }
266
+ },
267
+ [baseUrl, displayName, email, password],
268
+ );
269
+
270
+ if (!baseUrl) {
271
+ return (
272
+ <AuthPageShell
273
+ title="Create account"
274
+ description="Dev portal API is not configured."
275
+ >
276
+ <p className="text-sm text-muted-foreground">
277
+ Set authentication.apiBaseUrl to continue.
278
+ </p>
279
+ </AuthPageShell>
280
+ );
281
+ }
282
+
283
+ return (
284
+ <AuthPageShell
285
+ title="Create account"
286
+ description="Register for API access."
287
+ >
288
+ <form className="space-y-4" onSubmit={onSubmit}>
289
+ <div className="space-y-2">
290
+ <Label htmlFor="displayName">Name</Label>
291
+ <Input
292
+ id="displayName"
293
+ value={displayName}
294
+ onChange={(event) => setDisplayName(event.target.value)}
295
+ />
296
+ </div>
297
+ <div className="space-y-2">
298
+ <Label htmlFor="email">Email</Label>
299
+ <Input
300
+ id="email"
301
+ type="email"
302
+ autoComplete="email"
303
+ value={email}
304
+ onChange={(event) => setEmail(event.target.value)}
305
+ required
306
+ />
307
+ </div>
308
+ <div className="space-y-2">
309
+ <Label htmlFor="password">Password</Label>
310
+ <Input
311
+ id="password"
312
+ type="password"
313
+ autoComplete="new-password"
314
+ value={password}
315
+ onChange={(event) => setPassword(event.target.value)}
316
+ required
317
+ />
318
+ </div>
319
+ <AuthError message={error} />
320
+ {message ? (
321
+ <p className="text-sm text-muted-foreground">{message}</p>
322
+ ) : null}
323
+ <Button type="submit" className="w-full" disabled={pending}>
324
+ {pending ? <Spinner /> : "Create account"}
325
+ </Button>
326
+ <p className="text-sm text-center">
327
+ <Link
328
+ className="text-primary hover:underline"
329
+ to={`/login?redirect=${encodeURIComponent(returnUrl)}`}
330
+ >
331
+ Back to sign in
332
+ </Link>
333
+ </p>
334
+ </form>
335
+ </AuthPageShell>
336
+ );
337
+ }
338
+
339
+ export function DevPortalVerifyEmailPage({
340
+ apiBaseUrl,
341
+ }: {
342
+ apiBaseUrl?: string;
343
+ }) {
344
+ const baseUrl = useApiBaseUrl(apiBaseUrl);
345
+ const [search] = useSearchParams();
346
+ const [message, setMessage] = useState<string | null>(null);
347
+ const [error, setError] = useState<string | null>(null);
348
+
349
+ useEffect(() => {
350
+ const token = search.get("token");
351
+ if (!baseUrl || !token) return;
352
+ void postDevPortalAuth<{ message: string }>(
353
+ baseUrl,
354
+ "/api/v1/auth/verify-email",
355
+ {
356
+ token,
357
+ },
358
+ )
359
+ .then((response) => setMessage(response.message))
360
+ .catch((err) =>
361
+ setError(err instanceof Error ? err.message : "Verification failed."),
362
+ );
363
+ }, [baseUrl, search]);
364
+
365
+ return (
366
+ <AuthPageShell title="Verify email">
367
+ <AuthError message={error} />
368
+ {message ? (
369
+ <p className="text-sm text-muted-foreground">{message}</p>
370
+ ) : (
371
+ <Spinner />
372
+ )}
373
+ <Link className="text-sm text-primary hover:underline" to="/login">
374
+ Sign in
375
+ </Link>
376
+ </AuthPageShell>
377
+ );
378
+ }
379
+
380
+ export function DevPortalForgotPasswordPage({
381
+ apiBaseUrl,
382
+ }: {
383
+ apiBaseUrl?: string;
384
+ }) {
385
+ const baseUrl = useApiBaseUrl(apiBaseUrl);
386
+ const [email, setEmail] = useState("");
387
+ const [message, setMessage] = useState<string | null>(null);
388
+ const [error, setError] = useState<string | null>(null);
389
+ const [pending, setPending] = useState(false);
390
+
391
+ const onSubmit = useCallback(
392
+ async (event: React.FormEvent) => {
393
+ event.preventDefault();
394
+ if (!baseUrl) return;
395
+ setPending(true);
396
+ setError(null);
397
+ try {
398
+ const response = await postDevPortalAuth<{ message: string }>(
399
+ baseUrl,
400
+ "/api/v1/auth/forgot-password",
401
+ { email },
402
+ );
403
+ setMessage(response.message);
404
+ } catch (err) {
405
+ setError(err instanceof Error ? err.message : "Request failed.");
406
+ } finally {
407
+ setPending(false);
408
+ }
409
+ },
410
+ [baseUrl, email],
411
+ );
412
+
413
+ return (
414
+ <AuthPageShell
415
+ title="Reset password"
416
+ description="We'll email you a reset link."
417
+ >
418
+ <form className="space-y-4" onSubmit={onSubmit}>
419
+ <div className="space-y-2">
420
+ <Label htmlFor="email">Email</Label>
421
+ <Input
422
+ id="email"
423
+ type="email"
424
+ value={email}
425
+ onChange={(event) => setEmail(event.target.value)}
426
+ required
427
+ />
428
+ </div>
429
+ <AuthError message={error} />
430
+ {message ? (
431
+ <p className="text-sm text-muted-foreground">{message}</p>
432
+ ) : null}
433
+ <Button type="submit" className="w-full" disabled={pending}>
434
+ {pending ? <Spinner /> : "Send reset link"}
435
+ </Button>
436
+ </form>
437
+ </AuthPageShell>
438
+ );
439
+ }
440
+
441
+ export function DevPortalResetPasswordPage({
442
+ apiBaseUrl,
443
+ }: {
444
+ apiBaseUrl?: string;
445
+ }) {
446
+ const baseUrl = useApiBaseUrl(apiBaseUrl);
447
+ const [search] = useSearchParams();
448
+ const [password, setPassword] = useState("");
449
+ const [message, setMessage] = useState<string | null>(null);
450
+ const [error, setError] = useState<string | null>(null);
451
+ const [pending, setPending] = useState(false);
452
+
453
+ const onSubmit = useCallback(
454
+ async (event: React.FormEvent) => {
455
+ event.preventDefault();
456
+ const token = search.get("token");
457
+ if (!baseUrl || !token) return;
458
+ setPending(true);
459
+ setError(null);
460
+ try {
461
+ const response = await postDevPortalAuth<{ message: string }>(
462
+ baseUrl,
463
+ "/api/v1/auth/reset-password",
464
+ { token, newPassword: password },
465
+ );
466
+ setMessage(response.message);
467
+ } catch (err) {
468
+ setError(err instanceof Error ? err.message : "Reset failed.");
469
+ } finally {
470
+ setPending(false);
471
+ }
472
+ },
473
+ [baseUrl, password, search],
474
+ );
475
+
476
+ return (
477
+ <AuthPageShell title="Choose a new password">
478
+ <form className="space-y-4" onSubmit={onSubmit}>
479
+ <div className="space-y-2">
480
+ <Label htmlFor="password">New password</Label>
481
+ <Input
482
+ id="password"
483
+ type="password"
484
+ value={password}
485
+ onChange={(event) => setPassword(event.target.value)}
486
+ required
487
+ />
488
+ </div>
489
+ <AuthError message={error} />
490
+ {message ? (
491
+ <p className="text-sm text-muted-foreground">{message}</p>
492
+ ) : null}
493
+ <Button type="submit" className="w-full" disabled={pending}>
494
+ {pending ? <Spinner /> : "Update password"}
495
+ </Button>
496
+ </form>
497
+ </AuthPageShell>
498
+ );
499
+ }
@@ -19,5 +19,19 @@ export type EndUserMeResponse = {
19
19
 
20
20
  export type DevPortalAuthStatusResponse = {
21
21
  available: boolean;
22
- oidcConfigured: boolean;
22
+ nativeEnabled?: boolean;
23
+ oidcConfigured?: boolean;
24
+ providerCount?: number;
25
+ };
26
+
27
+ export type DevPortalAuthConfigResponse = {
28
+ native: { enabled: boolean };
29
+ email?: { provider: string; from: string } | null;
30
+ providers: Array<{
31
+ id: string;
32
+ displayName: string;
33
+ authority: string;
34
+ clientId: string;
35
+ scopes: string[];
36
+ }>;
23
37
  };
@@ -53,11 +53,15 @@ export function toAbsoluteReturnUrl(pathOrUrl: string): string {
53
53
  return new URL(pathOrUrl, window.location.origin).href;
54
54
  }
55
55
 
56
- export function buildDevPortalLoginUrl(
56
+ export function buildDevPortalOidcChallengeUrl(
57
57
  apiBaseUrl: string,
58
+ providerId: string,
58
59
  returnUrl: string,
59
60
  ): string {
60
- const url = new URL("/api/v1/auth/login", apiBaseUrl);
61
+ const url = new URL(
62
+ `/api/v1/auth/oidc/${encodeURIComponent(providerId)}/challenge`,
63
+ apiBaseUrl,
64
+ );
61
65
  url.searchParams.set("returnUrl", toAbsoluteReturnUrl(returnUrl));
62
66
  return url.toString();
63
67
  }
@@ -71,6 +75,127 @@ export function buildDevPortalLogoutUrl(
71
75
  return url.toString();
72
76
  }
73
77
 
78
+ export function normalizeDevPortalAuthConfig(
79
+ raw: unknown,
80
+ ): import("./dev-portal-constants.js").DevPortalAuthConfigResponse {
81
+ const data = (raw ?? {}) as Record<string, unknown>;
82
+ const nestedNative = data.native as { enabled?: boolean } | undefined;
83
+ const nativeEnabled =
84
+ typeof nestedNative?.enabled === "boolean"
85
+ ? nestedNative.enabled
86
+ : typeof data.nativeEnabled === "boolean"
87
+ ? data.nativeEnabled
88
+ : false;
89
+
90
+ const emailRaw = data.email;
91
+ const email =
92
+ emailRaw &&
93
+ typeof emailRaw === "object" &&
94
+ typeof (emailRaw as { provider?: unknown }).provider === "string" &&
95
+ typeof (emailRaw as { from?: unknown }).from === "string"
96
+ ? {
97
+ provider: (emailRaw as { provider: string }).provider,
98
+ from: (emailRaw as { from: string }).from,
99
+ }
100
+ : null;
101
+
102
+ const providers = Array.isArray(data.providers)
103
+ ? data.providers
104
+ .filter(
105
+ (provider): provider is Record<string, unknown> =>
106
+ provider != null && typeof provider === "object",
107
+ )
108
+ .map((provider) => ({
109
+ id: String(provider.id ?? ""),
110
+ displayName: String(provider.displayName ?? provider.id ?? ""),
111
+ authority: String(provider.authority ?? ""),
112
+ clientId: String(provider.clientId ?? ""),
113
+ scopes: Array.isArray(provider.scopes)
114
+ ? provider.scopes.map((scope) => String(scope))
115
+ : [],
116
+ }))
117
+ .filter((provider) => provider.id.length > 0)
118
+ : [];
119
+
120
+ return {
121
+ native: { enabled: nativeEnabled },
122
+ email,
123
+ providers,
124
+ };
125
+ }
126
+
127
+ export function isNativeAuthEnabled(
128
+ config:
129
+ | import("./dev-portal-constants.js").DevPortalAuthConfigResponse
130
+ | null
131
+ | undefined,
132
+ ): boolean {
133
+ if (!config) {
134
+ return false;
135
+ }
136
+
137
+ const raw =
138
+ config as import("./dev-portal-constants.js").DevPortalAuthConfigResponse & {
139
+ nativeEnabled?: boolean;
140
+ };
141
+
142
+ return raw.native?.enabled === true || raw.nativeEnabled === true;
143
+ }
144
+
145
+ export async function fetchDevPortalAuthConfig(
146
+ apiBaseUrl: string,
147
+ fetchImpl: typeof fetch = fetch,
148
+ ): Promise<import("./dev-portal-constants.js").DevPortalAuthConfigResponse> {
149
+ const response = await fetchImpl(`${apiBaseUrl}/api/v1/auth/config`, {
150
+ method: "GET",
151
+ headers: { Accept: "application/json" },
152
+ credentials: "include",
153
+ });
154
+
155
+ if (!response.ok) {
156
+ throw new Error("Failed to load auth configuration.");
157
+ }
158
+
159
+ return normalizeDevPortalAuthConfig(await response.json());
160
+ }
161
+
162
+ export async function postDevPortalAuth<T>(
163
+ apiBaseUrl: string,
164
+ path: string,
165
+ body: unknown,
166
+ fetchImpl: typeof fetch = fetch,
167
+ ): Promise<T> {
168
+ const response = await fetchImpl(`${apiBaseUrl}${path}`, {
169
+ method: "POST",
170
+ headers: {
171
+ Accept: "application/json",
172
+ "Content-Type": "application/json",
173
+ },
174
+ credentials: "include",
175
+ body: JSON.stringify(body),
176
+ });
177
+
178
+ if (response.status === 204) {
179
+ return {} as T;
180
+ }
181
+
182
+ if (!response.ok) {
183
+ let detail = "Request failed.";
184
+ try {
185
+ const problem = (await response.json()) as {
186
+ detail?: string;
187
+ title?: string;
188
+ };
189
+ detail = problem.detail ?? problem.title ?? detail;
190
+ } catch {
191
+ // ignore parse errors
192
+ }
193
+ throw new Error(detail);
194
+ }
195
+
196
+ return (await response.json()) as T;
197
+ }
198
+
74
199
  export async function probeDevPortalBackend(
75
200
  apiBaseUrl: string,
76
201
  fetchImpl: typeof fetch = fetch,
@@ -137,7 +262,7 @@ export function mapEndUserMeToProfile(
137
262
  raw.emailVerified,
138
263
  raw.EmailVerified,
139
264
  raw.email_verified,
140
- ) ?? (email != null ? true : false);
265
+ ) ?? email != null;
141
266
 
142
267
  return {
143
268
  sub: me.userId,