@lukoweb/apitogo 0.1.50 → 0.1.53

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 (36) hide show
  1. package/dist/cli/cli.js +1339 -922
  2. package/dist/declarations/config/apitogo-config-core.d.ts +25 -0
  3. package/dist/declarations/config/apitogo-config-loader.d.ts +2 -0
  4. package/dist/declarations/config/apitogo-config-loader.node.d.ts +4 -0
  5. package/dist/declarations/config/build-apitogo-config.d.ts +8 -0
  6. package/dist/declarations/config/loader.d.ts +4 -0
  7. package/dist/declarations/config/local-manifest.d.ts +16 -9
  8. package/dist/declarations/config/validators/HeaderNavigationSchema.d.ts +24 -24
  9. package/dist/declarations/config/validators/ModulesSchema.d.ts +38 -0
  10. package/dist/declarations/config/validators/ZudokuConfig.d.ts +18 -9
  11. package/dist/declarations/index.d.ts +2 -1
  12. package/dist/declarations/lib/authentication/auth-header-nav.d.ts +10 -0
  13. package/dist/declarations/lib/core/plugins.d.ts +1 -0
  14. package/dist/declarations/lib/ui/Button.d.ts +1 -1
  15. package/dist/declarations/lib/ui/Item.d.ts +1 -1
  16. package/dist/flat-config.d.ts +36 -23
  17. package/docs/configuration/authentication.md +11 -8
  18. package/docs/configuration/manifest.md +83 -93
  19. package/package.json +17 -1
  20. package/src/config/apitogo-config-core.ts +485 -0
  21. package/src/config/apitogo-config-loader.node.ts +68 -0
  22. package/src/config/apitogo-config-loader.ts +6 -0
  23. package/src/config/build-apitogo-config.ts +52 -0
  24. package/src/config/loader.ts +61 -1
  25. package/src/config/local-manifest.ts +72 -52
  26. package/src/config/resolve-modules.ts +16 -13
  27. package/src/config/validators/ModulesSchema.ts +17 -0
  28. package/src/index.ts +9 -1
  29. package/src/lib/authentication/auth-header-nav.ts +56 -0
  30. package/src/lib/authentication/providers/dev-portal-utils.ts +7 -2
  31. package/src/lib/components/Header.tsx +48 -60
  32. package/src/lib/components/MobileTopNavigation.tsx +29 -40
  33. package/src/lib/core/plugins.ts +1 -0
  34. package/src/vite/dev-portal-env.ts +12 -4
  35. package/src/vite/plugin-config.ts +20 -3
  36. package/src/vite/plugin-local-manifest.ts +15 -0
@@ -10,9 +10,25 @@ import {
10
10
  } from "vite";
11
11
  import { logger } from "../cli/common/logger.js";
12
12
  import { getZudokuRootDir } from "../cli/common/package-json.js";
13
+ import { applyAuthenticationHeaderNav } from "../lib/authentication/auth-header-nav.js";
13
14
  import { runPluginTransformConfig } from "../lib/core/transform-config.js";
14
15
  import invariant from "../lib/util/invariant.js";
16
+ import {
17
+ isBillingEnabled,
18
+ loadApitogoConfig,
19
+ readPlanCatalogItems,
20
+ } from "./apitogo-config-loader.js";
15
21
  import { fileExists } from "./file-exists.js";
22
+ import {
23
+ isAuthenticationEnabled,
24
+ manifestPathsForEnv,
25
+ manifestToPreviewCatalog,
26
+ plansToPreviewCatalog,
27
+ readEffectiveManifest,
28
+ resolveManifestEnv,
29
+ type DevPortalPreviewPlan,
30
+ type ManifestPlanInput,
31
+ } from "./local-manifest.js";
16
32
  import { resolveModulesConfig } from "./resolve-modules.js";
17
33
  import type { ResolvedModulesConfig } from "./validators/ModulesSchema.js";
18
34
  import type { ZudokuConfig } from "./validators/ZudokuConfig.js";
@@ -25,6 +41,9 @@ export type ConfigWithMeta = ZudokuConfig & {
25
41
  configPath: string;
26
42
  mode: typeof process.env.ZUDOKU_ENV;
27
43
  dependencies: string[];
44
+ pricingEnabled?: boolean;
45
+ authenticationEnabled?: boolean;
46
+ previewPlans?: DevPortalPreviewPlan[];
28
47
  };
29
48
  __resolvedModules: ResolvedModulesConfig;
30
49
  };
@@ -157,8 +176,17 @@ function isWatchableConfigDependency(depPath: string): boolean {
157
176
  }
158
177
 
159
178
  function getWatchableConfigDependencies(config: ConfigWithMeta): string[] {
179
+ const manifestEnv = resolveManifestEnv(
180
+ config.__meta.mode === "production" ? "production" : "development",
181
+ );
182
+ const { watchPaths } = manifestPathsForEnv(
183
+ config.__meta.rootDir,
184
+ manifestEnv,
185
+ );
186
+
160
187
  return [
161
188
  config.__meta.configPath,
189
+ ...watchPaths,
162
190
  ...config.__meta.dependencies.filter(isWatchableConfigDependency),
163
191
  ];
164
192
  }
@@ -171,6 +199,10 @@ async function hasConfigChanged() {
171
199
  try {
172
200
  const hasChanged = await Promise.all(
173
201
  files.map(async (depPath) => {
202
+ if (!(await fileExists(depPath))) {
203
+ return false;
204
+ }
205
+
174
206
  const depStat = await stat(depPath);
175
207
  const cachedMtime = modifiedTimes?.get(depPath);
176
208
  return !cachedMtime || depStat.mtimeMs !== cachedMtime;
@@ -192,6 +224,10 @@ async function updateModifiedTimes() {
192
224
 
193
225
  await Promise.all(
194
226
  files.map(async (depPath) => {
227
+ if (!(await fileExists(depPath))) {
228
+ return;
229
+ }
230
+
195
231
  const depStat = await stat(depPath);
196
232
  modifiedTimes?.set(depPath, depStat.mtimeMs);
197
233
  }),
@@ -225,9 +261,33 @@ export async function loadZudokuConfig(
225
261
 
226
262
  try {
227
263
  const loadedConfig = await loadZudokuConfigWithMeta(rootDir);
228
- const transformedConfig = await runPluginTransformConfig(loadedConfig);
264
+ const manifestEnv = resolveManifestEnv(configEnv.mode);
265
+ const manifest = await readEffectiveManifest(rootDir, manifestEnv);
266
+ const previewCatalog = manifestToPreviewCatalog(manifest);
267
+ const jsonConfig = loadApitogoConfig(rootDir);
268
+ const previewPlans = previewCatalog.plans.length
269
+ ? previewCatalog.plans
270
+ : plansToPreviewCatalog(
271
+ readPlanCatalogItems(jsonConfig) as ManifestPlanInput[],
272
+ ).plans;
273
+ const configWithManifestMeta: ConfigWithMeta = {
274
+ ...loadedConfig,
275
+ __meta: {
276
+ ...loadedConfig.__meta,
277
+ pricingEnabled: isBillingEnabled(jsonConfig),
278
+ authenticationEnabled: isAuthenticationEnabled(manifest),
279
+ previewPlans,
280
+ },
281
+ };
282
+ const transformedConfig = applyAuthenticationHeaderNav(
283
+ await runPluginTransformConfig(configWithManifestMeta),
284
+ );
229
285
  config = {
230
286
  ...transformedConfig,
287
+ __meta: {
288
+ ...transformedConfig.__meta,
289
+ ...configWithManifestMeta.__meta,
290
+ },
231
291
  __resolvedModules: resolveModulesConfig(transformedConfig),
232
292
  };
233
293
 
@@ -1,17 +1,25 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
-
5
- /** @deprecated Use {@link APITOGO_MANIFEST_ENV_FILES.development} — kept for external consumers. */
6
- export const DEV_PORTAL_MANIFEST_FILENAME = "apitogo.local.json";
7
-
4
+ import {
5
+ APITOGO_CONFIG_FILENAME,
6
+ extractManifest,
7
+ isBillingEnabled,
8
+ loadApitogoConfig,
9
+ readPlanCatalogItems,
10
+ } from "./apitogo-config-loader.js";
11
+
12
+ /** @deprecated Legacy manifest filenames — apitogo.config.json is the only supported source. */
8
13
  export const APITOGO_MANIFEST_BASE_FILENAME = "apitogo.json";
9
14
 
15
+ /** @deprecated Legacy per-environment manifest overrides. */
10
16
  export const APITOGO_MANIFEST_ENV_FILES = {
11
17
  development: "apitogo.local.json",
12
18
  production: "apitogo.prod.json",
13
19
  } as const;
14
20
 
21
+ export { APITOGO_CONFIG_FILENAME };
22
+
15
23
  export type ApitogoManifestEnv = keyof typeof APITOGO_MANIFEST_ENV_FILES;
16
24
 
17
25
  const BillingPeriodSchema = z.enum(["month", "year", "monthly", "annual"]);
@@ -19,14 +27,18 @@ const BillingPeriodSchema = z.enum(["month", "year", "monthly", "annual"]);
19
27
  export const ManifestPlanInputSchema = z.object({
20
28
  sku: z.string().min(1),
21
29
  displayName: z.string().min(1),
22
- isFree: z.boolean(),
23
- priceAmount: z.number().optional(),
30
+ priceAmount: z.number().default(0),
24
31
  priceCurrency: z.string().optional(),
25
32
  billingPeriod: BillingPeriodSchema.optional(),
26
33
  limitsJson: z.string().optional(),
27
34
  includedActionKeys: z.array(z.string()).optional(),
28
35
  });
29
36
 
37
+ /** Free plans have zero price; paid plans have priceAmount greater than zero. */
38
+ export function isPlanFree(plan: { priceAmount?: number }): boolean {
39
+ return (plan.priceAmount ?? 0) <= 0;
40
+ }
41
+
30
42
  export const DevPortalLocalManifestSchema = z.object({
31
43
  siteName: z.string().optional(),
32
44
  branding: z
@@ -40,6 +52,13 @@ export const DevPortalLocalManifestSchema = z.object({
40
52
  enabled: z.boolean().optional(),
41
53
  })
42
54
  .optional(),
55
+ authentication: z
56
+ .object({
57
+ enabled: z.boolean().optional(),
58
+ type: z.string().optional(),
59
+ apiBaseUrl: z.string().optional(),
60
+ })
61
+ .optional(),
43
62
  plans: z.array(ManifestPlanInputSchema).optional(),
44
63
  backend: z
45
64
  .object({
@@ -62,7 +81,6 @@ export const DevPortalLocalManifestSchema = z.object({
62
81
  })
63
82
  .optional(),
64
83
  projectId: z.string().optional(),
65
- projectName: z.string().optional(),
66
84
  });
67
85
 
68
86
  export type ManifestPlanInput = z.infer<typeof ManifestPlanInputSchema>;
@@ -73,7 +91,6 @@ export type DevPortalLocalManifest = z.infer<
73
91
  export type DevPortalPreviewPlan = {
74
92
  sku: string;
75
93
  displayName: string;
76
- isFree: boolean;
77
94
  priceAmount: number;
78
95
  priceCurrency: string;
79
96
  billingPeriod: string;
@@ -83,6 +100,7 @@ export type DevPortalPreviewPlan = {
83
100
 
84
101
  export type DevPortalPreviewPlanCatalog = {
85
102
  pricingEnabled: boolean;
103
+ authenticationEnabled: boolean;
86
104
  plans: DevPortalPreviewPlan[];
87
105
  };
88
106
 
@@ -92,6 +110,12 @@ export function isPricingEnabled(
92
110
  return manifest?.pricing?.enabled === true;
93
111
  }
94
112
 
113
+ export function isAuthenticationEnabled(
114
+ manifest: DevPortalLocalManifest | null | undefined,
115
+ ): boolean {
116
+ return manifest?.authentication?.enabled === true;
117
+ }
118
+
95
119
  export function manifestToPreviewCatalog(
96
120
  manifest: DevPortalLocalManifest | null | undefined,
97
121
  ): DevPortalPreviewPlanCatalog {
@@ -99,6 +123,7 @@ export function manifestToPreviewCatalog(
99
123
  const catalog = plansToPreviewCatalog(plans);
100
124
  return {
101
125
  pricingEnabled: isPricingEnabled(manifest),
126
+ authenticationEnabled: isAuthenticationEnabled(manifest),
102
127
  plans: catalog.plans,
103
128
  };
104
129
  }
@@ -129,7 +154,6 @@ export function plansToPreviewCatalog(
129
154
  plans: plans.map((plan) => ({
130
155
  sku: plan.sku,
131
156
  displayName: plan.displayName,
132
- isFree: plan.isFree,
133
157
  priceAmount: plan.priceAmount ?? 0,
134
158
  priceCurrency: plan.priceCurrency ?? "usd",
135
159
  billingPeriod: normalizeBillingPeriod(plan.billingPeriod),
@@ -170,6 +194,12 @@ export function mergeManifest(
170
194
  if (override.pricing) {
171
195
  merged.pricing = { ...base.pricing, ...override.pricing };
172
196
  }
197
+ if (override.authentication) {
198
+ merged.authentication = {
199
+ ...base.authentication,
200
+ ...override.authentication,
201
+ };
202
+ }
173
203
  if (override.backend) {
174
204
  merged.backend = { ...base.backend, ...override.backend };
175
205
  }
@@ -184,24 +214,20 @@ export function mergeManifest(
184
214
  }
185
215
 
186
216
  export function manifestFilePath(rootDir: string, filename?: string): string {
187
- return path.join(
188
- path.resolve(rootDir),
189
- filename ?? DEV_PORTAL_MANIFEST_FILENAME,
190
- );
217
+ return path.join(path.resolve(rootDir), filename ?? APITOGO_CONFIG_FILENAME);
191
218
  }
192
219
 
193
220
  export function manifestPathsForEnv(
194
221
  rootDir: string,
195
- env: ApitogoManifestEnv,
222
+ _env: ApitogoManifestEnv,
196
223
  ): { basePath: string; overridePath: string; watchPaths: string[] } {
197
224
  const resolvedRoot = path.resolve(rootDir);
198
- const basePath = path.join(resolvedRoot, APITOGO_MANIFEST_BASE_FILENAME);
199
- const overridePath = path.join(resolvedRoot, APITOGO_MANIFEST_ENV_FILES[env]);
225
+ const configPath = path.join(resolvedRoot, APITOGO_CONFIG_FILENAME);
200
226
 
201
227
  return {
202
- basePath,
203
- overridePath,
204
- watchPaths: [basePath, overridePath],
228
+ basePath: configPath,
229
+ overridePath: configPath,
230
+ watchPaths: [configPath],
205
231
  };
206
232
  }
207
233
 
@@ -243,48 +269,42 @@ export async function readManifestFile(
243
269
  }
244
270
  }
245
271
 
272
+ function parseManifestRecord(
273
+ manifest: ReturnType<typeof extractManifest>,
274
+ ): DevPortalLocalManifest | null {
275
+ if (!manifest) {
276
+ return null;
277
+ }
278
+
279
+ const result = DevPortalLocalManifestSchema.safeParse(manifest);
280
+ return result.success ? result.data : null;
281
+ }
282
+
246
283
  export async function readEffectiveManifest(
247
284
  rootDir: string,
248
- env: ApitogoManifestEnv,
285
+ _env: ApitogoManifestEnv,
249
286
  ): Promise<DevPortalLocalManifest | null> {
250
- const { basePath, overridePath } = manifestPathsForEnv(rootDir, env);
251
- const hasBase = await fileExists(basePath);
252
- const hasOverride = await fileExists(overridePath);
253
-
254
- if (!hasBase) {
255
- const legacy = await readManifestFile(
256
- rootDir,
257
- APITOGO_MANIFEST_ENV_FILES.development,
258
- );
259
- if (legacy) {
260
- return legacy;
287
+ try {
288
+ const config = loadApitogoConfig(rootDir);
289
+ const manifest = parseManifestRecord(extractManifest(config));
290
+ if (!manifest) {
291
+ return null;
261
292
  }
262
293
 
263
- if (hasOverride) {
264
- return readManifestFile(rootDir, APITOGO_MANIFEST_ENV_FILES[env]);
294
+ if (!manifest.plans?.length) {
295
+ const items = readPlanCatalogItems(config);
296
+ if (items.length) {
297
+ manifest.plans = items as ManifestPlanInput[];
298
+ }
265
299
  }
266
300
 
301
+ return {
302
+ ...manifest,
303
+ pricing: { ...manifest.pricing, enabled: isBillingEnabled(config) },
304
+ };
305
+ } catch {
267
306
  return null;
268
307
  }
269
-
270
- const base = await readManifestFile(rootDir, APITOGO_MANIFEST_BASE_FILENAME);
271
- if (!base) {
272
- return null;
273
- }
274
-
275
- if (!hasOverride) {
276
- return base;
277
- }
278
-
279
- const override = await readManifestFile(
280
- rootDir,
281
- APITOGO_MANIFEST_ENV_FILES[env],
282
- );
283
- if (!override) {
284
- return base;
285
- }
286
-
287
- return mergeManifest(base, override);
288
308
  }
289
309
 
290
310
  export async function readLocalManifest(
@@ -141,10 +141,14 @@ export const resolveModulesConfig = (
141
141
  : !!config.authentication;
142
142
 
143
143
  const apiKeysEnabled = explicit
144
- ? (apiKeysModuleConfig?.enabled ?? config.apiKeys?.enabled ?? false)
145
- : (config.apiKeys?.enabled ?? false);
144
+ ? (apiKeysModuleConfig?.enabled ?? false)
145
+ : false;
146
146
 
147
- const plansEnabled = plansConfig?.enabled ?? false;
147
+ const billingEnabled =
148
+ (config as { __meta?: { pricingEnabled?: boolean } }).__meta
149
+ ?.pricingEnabled === true || plansConfig?.enabled === true;
150
+
151
+ const plansEnabled = billingEnabled;
148
152
 
149
153
  const userPanelEnabled =
150
154
  userPanelModule?.enabled ??
@@ -158,11 +162,10 @@ export const resolveModulesConfig = (
158
162
  return {
159
163
  docs: {
160
164
  enabled: docsEnabled,
161
- files: docsModule?.files ?? config.docs?.files,
162
- publishMarkdown:
163
- docsModule?.publishMarkdown ?? config.docs?.publishMarkdown,
164
- defaultOptions: docsModule?.defaultOptions ?? config.docs?.defaultOptions,
165
- llms: docsModule?.llms ?? config.docs?.llms,
165
+ files: docsModule?.files,
166
+ publishMarkdown: docsModule?.publishMarkdown,
167
+ defaultOptions: docsModule?.defaultOptions,
168
+ llms: docsModule?.llms,
166
169
  },
167
170
  landing: {
168
171
  enabled: landingEnabled,
@@ -203,6 +206,7 @@ export const resolveModulesConfig = (
203
206
  enabled: plansEnabled,
204
207
  path: joinPanelPath(basePath, plansConfig?.path ?? "plans"),
205
208
  content: resolvePlansContent(plansConfig?.content),
209
+ items: plansConfig?.items,
206
210
  },
207
211
  },
208
212
  };
@@ -242,11 +246,10 @@ export const getEffectiveDocsConfig = (
242
246
  }
243
247
 
244
248
  return {
245
- files: resolved.docs.files ?? config.docs?.files ?? [DEFAULT_DOCS_FILES],
246
- publishMarkdown:
247
- resolved.docs.publishMarkdown ?? config.docs?.publishMarkdown ?? false,
248
- defaultOptions: resolved.docs.defaultOptions ?? config.docs?.defaultOptions,
249
- llms: resolved.docs.llms ?? config.docs?.llms,
249
+ files: resolved.docs.files ?? [DEFAULT_DOCS_FILES],
250
+ publishMarkdown: resolved.docs.publishMarkdown ?? false,
251
+ defaultOptions: resolved.docs.defaultOptions,
252
+ llms: resolved.docs.llms,
250
253
  };
251
254
  };
252
255
 
@@ -154,6 +154,16 @@ export const ApiKeysModuleSchema = ModuleToggleSchema.extend({
154
154
  .describe("Route path for API key management."),
155
155
  });
156
156
 
157
+ const PlanCatalogItemSchema = z.object({
158
+ sku: z.string(),
159
+ displayName: z.string(),
160
+ priceAmount: z.number().default(0),
161
+ priceCurrency: z.string().optional(),
162
+ billingPeriod: z.string().optional(),
163
+ limitsJson: z.string().optional(),
164
+ includedActionKeys: z.array(z.string()).optional(),
165
+ });
166
+
157
167
  export const PlansModuleSchema = ModuleToggleSchema.extend({
158
168
  path: z
159
169
  .string()
@@ -164,6 +174,12 @@ export const PlansModuleSchema = ModuleToggleSchema.extend({
164
174
  content: PlansContentSchema.describe(
165
175
  "Default plans page content when no custom component is provided.",
166
176
  ),
177
+ items: z
178
+ .array(PlanCatalogItemSchema)
179
+ .optional()
180
+ .describe(
181
+ "Gateway plan catalog for /pricing preview and publish (canonical storage).",
182
+ ),
167
183
  });
168
184
 
169
185
  export const DashboardModuleSchema = ModuleToggleSchema.extend({
@@ -288,6 +304,7 @@ export type ResolvedPlansModule = {
288
304
  enabled: boolean;
289
305
  path: string;
290
306
  content: ResolvedPlansContent;
307
+ items?: z.infer<typeof PlanCatalogItemSchema>[];
291
308
  };
292
309
 
293
310
  export type ResolvedDashboardModule = {
package/src/index.ts CHANGED
@@ -71,4 +71,12 @@ export {
71
71
  type ProblemJson,
72
72
  throwIfProblemJson,
73
73
  } from "./lib/util/problemJson.js";
74
- export type { MdxComponentsType } from "./lib/util/MdxComponents.js";
74
+ export { buildApitogoConfig } from "./config/build-apitogo-config.js";
75
+ export {
76
+ APITOGO_CONFIG_FILENAME,
77
+ applyEnvOverrides,
78
+ extractManifest,
79
+ extractSiteConfig,
80
+ normalizeApitogoConfig,
81
+ resolveProjectDisplayName,
82
+ } from "./config/apitogo-config-core.js";
@@ -0,0 +1,56 @@
1
+ import type { HeaderNavigation } from "../../config/validators/HeaderNavigationSchema.js";
2
+ import type { ZudokuConfig } from "../../config/validators/ZudokuConfig.js";
3
+
4
+ export const LOGIN_NAV_PATH = "/signin";
5
+
6
+ export const LOGIN_HEADER_NAV_ITEM = {
7
+ label: "Login",
8
+ to: LOGIN_NAV_PATH,
9
+ display: "anon" as const,
10
+ };
11
+
12
+ export function withoutLoginNavigation(
13
+ navigation: HeaderNavigation,
14
+ ): HeaderNavigation {
15
+ return navigation.filter(
16
+ (item) => !("to" in item && item.to === LOGIN_NAV_PATH),
17
+ );
18
+ }
19
+
20
+ export function applyAuthenticationHeaderNav<T extends ZudokuConfig>(
21
+ config: T,
22
+ ): T {
23
+ if (!config.authentication) {
24
+ return config;
25
+ }
26
+
27
+ const authNavEnabled = config.__meta?.authenticationEnabled === true;
28
+ const navigation = config.header?.navigation ?? [];
29
+
30
+ if (!authNavEnabled) {
31
+ const filtered = withoutLoginNavigation(navigation);
32
+ if (filtered.length === navigation.length) {
33
+ return config;
34
+ }
35
+
36
+ return {
37
+ ...config,
38
+ header: {
39
+ ...config.header,
40
+ navigation: filtered,
41
+ },
42
+ };
43
+ }
44
+
45
+ if (navigation.some((item) => "to" in item && item.to === LOGIN_NAV_PATH)) {
46
+ return config;
47
+ }
48
+
49
+ return {
50
+ ...config,
51
+ header: {
52
+ ...config.header,
53
+ navigation: [...navigation, LOGIN_HEADER_NAV_ITEM],
54
+ },
55
+ };
56
+ }
@@ -5,6 +5,8 @@ import {
5
5
  type EndUserMeResponse,
6
6
  } from "./dev-portal-constants.js";
7
7
 
8
+ declare const VITE_APITOGO_AUTHENTICATION_API_BASE_URL: string | undefined;
9
+ /** @deprecated Use {@link VITE_APITOGO_AUTHENTICATION_API_BASE_URL}. */
8
10
  declare const VITE_APITOGO_DEV_PORTAL_API_URL: string | undefined;
9
11
 
10
12
  export function resolveDevPortalApiBaseUrl(
@@ -16,9 +18,12 @@ export function resolveDevPortalApiBaseUrl(
16
18
  }
17
19
 
18
20
  const fromEnv =
19
- typeof VITE_APITOGO_DEV_PORTAL_API_URL === "string"
21
+ (typeof VITE_APITOGO_AUTHENTICATION_API_BASE_URL === "string"
22
+ ? VITE_APITOGO_AUTHENTICATION_API_BASE_URL.trim()
23
+ : "") ||
24
+ (typeof VITE_APITOGO_DEV_PORTAL_API_URL === "string"
20
25
  ? VITE_APITOGO_DEV_PORTAL_API_URL.trim()
21
- : "";
26
+ : "");
22
27
 
23
28
  return fromEnv ? fromEnv.replace(/\/+$/, "") : undefined;
24
29
  }
@@ -1,5 +1,4 @@
1
1
  import { Button } from "@lukoweb/apitogo/ui/Button.js";
2
- import { Skeleton } from "@lukoweb/apitogo/ui/Skeleton.js";
3
2
  import { LogOutIcon } from "lucide-react";
4
3
  import { lazy, memo, Suspense } from "react";
5
4
  import { Link } from "react-router";
@@ -20,7 +19,6 @@ import {
20
19
  import { cn } from "../util/cn.js";
21
20
  import { joinUrl } from "../util/joinUrl.js";
22
21
  import { Banner } from "./Banner.js";
23
- import { ClientOnly } from "./ClientOnly.js";
24
22
  import { useZudoku } from "./context/ZudokuContext.js";
25
23
  import { MobileTopNavigation } from "./MobileTopNavigation.js";
26
24
  import { PageProgress } from "./PageProgress.js";
@@ -73,68 +71,58 @@ const ProfileMenu = () => {
73
71
  const context = useZudoku();
74
72
  const profileItems = context.getProfileMenuItems();
75
73
  const auth = useAuth();
76
- const { isAuthEnabled, isAuthenticated, isPending, profile } = auth;
74
+ const { isAuthEnabled, isAuthenticated, profile } = auth;
77
75
 
78
- if (!isAuthEnabled) return null;
76
+ if (!isAuthEnabled || !isAuthenticated) return null;
79
77
 
80
78
  return (
81
- <ClientOnly fallback={<Skeleton className="rounded-sm h-5 w-24 mr-4" />}>
82
- {isPending ? (
83
- <Skeleton className="rounded-sm h-9 w-24" />
84
- ) : !isAuthenticated ? (
85
- <Button size="lg" variant="ghost" onClick={() => auth.login()}>
86
- Login
79
+ <DropdownMenu modal={false}>
80
+ <DropdownMenuTrigger asChild>
81
+ <Button size="lg" variant="ghost">
82
+ {profile?.name ?? "My Account"}
87
83
  </Button>
88
- ) : (
89
- <DropdownMenu modal={false}>
90
- <DropdownMenuTrigger asChild>
91
- <Button size="lg" variant="ghost">
92
- {profile?.name ?? "My Account"}
93
- </Button>
94
- </DropdownMenuTrigger>
95
- <DropdownMenuContent className="w-56">
96
- <DropdownMenuLabel>
97
- {profile?.name ?? "My Account"}
98
- {profile?.email && profile.email !== profile?.name && (
99
- <div className="font-normal text-muted-foreground">
100
- {profile.email}
101
- </div>
102
- )}
103
- </DropdownMenuLabel>
104
- {profileItems.filter((i) => i.category === "top").length > 0 && (
105
- <DropdownMenuSeparator />
106
- )}
107
- {profileItems
108
- .filter((i) => i.category === "top")
109
- .map((i) => (
110
- <RecursiveMenu key={i.label} item={i} />
111
- ))}
112
- {profileItems.filter((i) => !i.category || i.category === "middle")
113
- .length > 0 && <DropdownMenuSeparator />}
114
- {profileItems
115
- .filter((i) => !i.category || i.category === "middle")
116
- .map((i) => (
117
- <RecursiveMenu key={i.label} item={i} />
118
- ))}
119
- {profileItems.filter((i) => i.category === "bottom").length > 0 && (
120
- <DropdownMenuSeparator />
121
- )}
122
- {profileItems
123
- .filter((i) => i.category === "bottom")
124
- .map((i) => (
125
- <RecursiveMenu key={i.label} item={i} />
126
- ))}
127
- <DropdownMenuSeparator />
128
- <Link to="/signout">
129
- <DropdownMenuItem className="flex gap-2">
130
- <LogOutIcon size={16} strokeWidth={1} absoluteStrokeWidth />
131
- Logout
132
- </DropdownMenuItem>
133
- </Link>
134
- </DropdownMenuContent>
135
- </DropdownMenu>
136
- )}
137
- </ClientOnly>
84
+ </DropdownMenuTrigger>
85
+ <DropdownMenuContent className="w-56">
86
+ <DropdownMenuLabel>
87
+ {profile?.name ?? "My Account"}
88
+ {profile?.email && profile.email !== profile?.name && (
89
+ <div className="font-normal text-muted-foreground">
90
+ {profile.email}
91
+ </div>
92
+ )}
93
+ </DropdownMenuLabel>
94
+ {profileItems.filter((i) => i.category === "top").length > 0 && (
95
+ <DropdownMenuSeparator />
96
+ )}
97
+ {profileItems
98
+ .filter((i) => i.category === "top")
99
+ .map((i) => (
100
+ <RecursiveMenu key={i.label} item={i} />
101
+ ))}
102
+ {profileItems.filter((i) => !i.category || i.category === "middle")
103
+ .length > 0 && <DropdownMenuSeparator />}
104
+ {profileItems
105
+ .filter((i) => !i.category || i.category === "middle")
106
+ .map((i) => (
107
+ <RecursiveMenu key={i.label} item={i} />
108
+ ))}
109
+ {profileItems.filter((i) => i.category === "bottom").length > 0 && (
110
+ <DropdownMenuSeparator />
111
+ )}
112
+ {profileItems
113
+ .filter((i) => i.category === "bottom")
114
+ .map((i) => (
115
+ <RecursiveMenu key={i.label} item={i} />
116
+ ))}
117
+ <DropdownMenuSeparator />
118
+ <Link to="/signout">
119
+ <DropdownMenuItem className="flex gap-2">
120
+ <LogOutIcon size={16} strokeWidth={1} absoluteStrokeWidth />
121
+ Logout
122
+ </DropdownMenuItem>
123
+ </Link>
124
+ </DropdownMenuContent>
125
+ </DropdownMenu>
138
126
  );
139
127
  };
140
128
  export const Header = memo(function HeaderInner() {