@lukoweb/apitogo 0.1.55 → 0.1.57

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.
@@ -64,7 +64,7 @@ export type _Schema15 = boolean
64
64
  /**
65
65
  * Gateway plan catalog for /pricing preview and publish (canonical storage).
66
66
  */
67
- export type _Schema35 = {
67
+ export type _Schema37 = {
68
68
  sku: string
69
69
  displayName: string
70
70
  priceAmount?: number
@@ -73,31 +73,31 @@ export type _Schema35 = {
73
73
  limitsJson?: string
74
74
  includedActionKeys?: string[]
75
75
  }[]
76
- export type _Schema36 = ({
76
+ export type _Schema38 = ({
77
77
  type: "url"
78
- input: (string | _Schema37[])
79
- server?: _Schema38
80
- path?: _Schema39
81
- categories?: _Schema40
82
- options?: _Schema42
78
+ input: (string | _Schema39[])
79
+ server?: _Schema40
80
+ path?: _Schema41
81
+ categories?: _Schema42
82
+ options?: _Schema44
83
83
  } | {
84
84
  type: "file"
85
- input: (string | (string | _Schema37)[])
86
- server?: _Schema38
87
- path?: _Schema39
88
- categories?: _Schema40
89
- options?: _Schema42
85
+ input: (string | (string | _Schema39)[])
86
+ server?: _Schema40
87
+ path?: _Schema41
88
+ categories?: _Schema42
89
+ options?: _Schema44
90
90
  } | {
91
91
  type: "raw"
92
92
  input: string
93
- server?: _Schema38
94
- path?: _Schema39
95
- categories?: _Schema40
96
- options?: _Schema42
93
+ server?: _Schema40
94
+ path?: _Schema41
95
+ categories?: _Schema42
96
+ options?: _Schema44
97
97
  })
98
- export type _Schema38 = string
99
- export type _Schema39 = string
100
- export type _Schema40 = {
98
+ export type _Schema40 = string
99
+ export type _Schema41 = string
100
+ export type _Schema42 = {
101
101
  label: string
102
102
  tags: string[]
103
103
  }[]
@@ -396,10 +396,10 @@ export interface FlatZudokuConfig {
396
396
  modules?: {
397
397
  docs?: _Schema14
398
398
  landing?: _Schema16
399
- userPanel?: _Schema23
399
+ userPanel?: _Schema25
400
400
  }
401
- apis?: (_Schema36 | _Schema36[])
402
- catalogs?: (_Schema43 | _Schema43[])
401
+ apis?: (_Schema38 | _Schema38[])
402
+ catalogs?: (_Schema45 | _Schema45[])
403
403
  apiKeys?: {
404
404
  enabled: boolean
405
405
  getConsumers?: unknown
@@ -427,7 +427,7 @@ export interface FlatZudokuConfig {
427
427
  }
428
428
  enableStatusPages?: boolean
429
429
  defaults?: {
430
- apis: _Schema42
430
+ apis: _Schema44
431
431
  examplesLanguage?: string
432
432
  }
433
433
  __pluginDirs?: string[]
@@ -525,6 +525,10 @@ export interface _Schema16 {
525
525
  */
526
526
  layout?: ("default" | "landing" | "none")
527
527
  content?: _Schema21
528
+ /**
529
+ * When true, merge DEFAULT_LANDING_CONTENT for unset sections. When false or omitted, only configured sections are shown.
530
+ */
531
+ useDefaults?: boolean
528
532
  }
529
533
  /**
530
534
  * Default landing page content when no custom component is provided.
@@ -532,6 +536,10 @@ export interface _Schema16 {
532
536
  export interface _Schema21 {
533
537
  hero?: {
534
538
  title?: string
539
+ /**
540
+ * Accent-colored word or phrase shown on a new line after the title.
541
+ */
542
+ titleAccent?: string
535
543
  subtitle?: string
536
544
  description?: string
537
545
  badge?: string
@@ -542,13 +550,18 @@ export interface _Schema21 {
542
550
  code: string
543
551
  }
544
552
  }
553
+ featuresSection?: {
554
+ label?: string
555
+ title?: string
556
+ description?: string
557
+ }
545
558
  features?: {
546
559
  title: string
547
560
  description: string
548
561
  icon?: string
549
562
  }[]
550
- primaryAction?: _Schema22
551
- secondaryAction?: _Schema22
563
+ primaryAction?: _Schema23
564
+ secondaryAction?: _Schema23
552
565
  ticker?: {
553
566
  enabled?: boolean
554
567
  items?: {
@@ -581,19 +594,19 @@ export interface _Schema21 {
581
594
  enabled?: boolean
582
595
  title?: string
583
596
  description?: string
584
- primaryAction?: _Schema22
585
- secondaryAction?: _Schema22
597
+ primaryAction?: _Schema23
598
+ secondaryAction?: _Schema23
586
599
  }
587
600
  showPoweredBy?: boolean
588
601
  }
589
- export interface _Schema22 {
602
+ export interface _Schema23 {
590
603
  label: string
591
604
  href: string
592
605
  }
593
606
  /**
594
607
  * User panel module — authenticated user area with sub-modules.
595
608
  */
596
- export interface _Schema23 {
609
+ export interface _Schema25 {
597
610
  enabled?: _Schema15
598
611
  /**
599
612
  * Base path prefix for user panel routes.
@@ -615,7 +628,7 @@ export interface _Schema23 {
615
628
  component?: {
616
629
  [k: string]: unknown
617
630
  }
618
- content?: _Schema30
631
+ content?: _Schema32
619
632
  }
620
633
  userManagement?: {
621
634
  enabled?: _Schema15
@@ -635,7 +648,7 @@ export interface _Schema23 {
635
648
  component?: {
636
649
  [k: string]: unknown
637
650
  }
638
- content?: _Schema31
651
+ content?: _Schema33
639
652
  }
640
653
  apiKeys?: {
641
654
  enabled?: _Schema15
@@ -660,30 +673,30 @@ export interface _Schema23 {
660
673
  component?: {
661
674
  [k: string]: unknown
662
675
  }
663
- content?: _Schema34
664
- items?: _Schema35
676
+ content?: _Schema36
677
+ items?: _Schema37
665
678
  }
666
679
  }
667
680
  /**
668
681
  * Default dashboard page content when no custom component is provided.
669
682
  */
670
- export interface _Schema30 {
683
+ export interface _Schema32 {
671
684
  title?: string
672
685
  subtitle?: string
673
686
  description?: string
674
- quickLinks?: _Schema22[]
687
+ quickLinks?: _Schema23[]
675
688
  }
676
689
  /**
677
690
  * Default profile page content when no custom component is provided.
678
691
  */
679
- export interface _Schema31 {
692
+ export interface _Schema33 {
680
693
  title?: string
681
694
  description?: string
682
695
  }
683
696
  /**
684
697
  * Default plans page content when no custom component is provided.
685
698
  */
686
- export interface _Schema34 {
699
+ export interface _Schema36 {
687
700
  title?: string
688
701
  description?: string
689
702
  tiers?: {
@@ -692,12 +705,12 @@ export interface _Schema34 {
692
705
  description?: string
693
706
  }[]
694
707
  }
695
- export interface _Schema37 {
708
+ export interface _Schema39 {
696
709
  path: string
697
710
  input: string
698
711
  label?: string
699
712
  }
700
- export interface _Schema42 {
713
+ export interface _Schema44 {
701
714
  examplesLanguage?: string
702
715
  supportedLanguages?: {
703
716
  value: string
@@ -714,7 +727,7 @@ export interface _Schema42 {
714
727
  transformExamples?: unknown
715
728
  generateCodeSnippet?: unknown
716
729
  }
717
- export interface _Schema43 {
730
+ export interface _Schema45 {
718
731
  path: string
719
732
  label: string
720
733
  items?: string[]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -0,0 +1,154 @@
1
+ import type { ResolvedLandingContent } from "./validators/ModulesSchema.js";
2
+
3
+ /** Default landing sections aligned with webapp-template/APIToGo.dc.html */
4
+ export const DEFAULT_LANDING_CONTENT: ResolvedLandingContent = {
5
+ hero: {
6
+ badge: "99.98% uptime",
7
+ subtitle: "Production-ready APIs, one endpoint away",
8
+ title: "Developer APIs that ship",
9
+ titleAccent: "today.",
10
+ description:
11
+ "Documentation, interactive API reference, auth, and billing — sub-40ms responses, one API key, generous free tier.",
12
+ highlights: ["No credit card", "10k requests free", "SDKs for 6 languages"],
13
+ codeExample: {
14
+ filename: "example.sh",
15
+ language: "bash",
16
+ code: `$ curl https://api.example.com/v1/items \\
17
+ -H "Authorization: Bearer YOUR_API_KEY" \\
18
+ -d "limit=10"
19
+
20
+ {
21
+ "items": [
22
+ { "id": "item_1", "name": "Widget", "status": "active" },
23
+ { "id": "item_2", "name": "Gadget", "status": "active" }
24
+ ]
25
+ } // 28ms`,
26
+ },
27
+ },
28
+ primaryAction: { label: "Get your API key", href: "/register" },
29
+ secondaryAction: { label: "Read the docs", href: "/introduction" },
30
+ ticker: {
31
+ items: [
32
+ { label: "GET", value: "/v1/items", change: "28ms" },
33
+ { label: "POST", value: "/v1/items", change: "34ms" },
34
+ { label: "GET", value: "/v1/users", change: "19ms" },
35
+ { label: "PATCH", value: "/v1/users/{id}", change: "41ms" },
36
+ { label: "GET", value: "/v1/health", change: "16ms" },
37
+ { label: "DELETE", value: "/v1/items/{id}", change: "22ms" },
38
+ { label: "GET", value: "/v1/search", change: "47ms" },
39
+ { label: "POST", value: "/v1/auth/token", change: "52ms" },
40
+ ],
41
+ },
42
+ trust: {
43
+ title: "Trusted by teams shipping production APIs",
44
+ logos: ["Acme Corp", "BuildFast", "DataFlow", "ShipKit", "DevStack", "LaunchPad"],
45
+ },
46
+ featuresSection: {
47
+ label: "// why us",
48
+ title: "Everything you need to build on your API",
49
+ description:
50
+ "Skip the infra. We handle docs, auth, keys, and billing so you can focus on your product.",
51
+ },
52
+ features: [
53
+ {
54
+ icon: "gauge",
55
+ title: "Sub-40ms responses",
56
+ description:
57
+ "Edge-cached and globally distributed, so your app feels instant anywhere.",
58
+ },
59
+ {
60
+ icon: "radio",
61
+ title: "Real-time streams",
62
+ description:
63
+ "WebSocket feeds with automatic reconnection for live data and events.",
64
+ },
65
+ {
66
+ icon: "blocks",
67
+ title: "Unified schema",
68
+ description:
69
+ "One consistent API design — no per-endpoint glue code or custom adapters.",
70
+ },
71
+ {
72
+ icon: "shield-check",
73
+ title: "99.98% uptime",
74
+ description:
75
+ "Redundant infra with a public status page and an SLA on paid plans.",
76
+ },
77
+ {
78
+ icon: "code",
79
+ title: "SDKs for 6 languages",
80
+ description:
81
+ "Typed clients for JS, Python, Go, Rust, Ruby and PHP. Copy, paste, ship.",
82
+ },
83
+ {
84
+ icon: "webhook",
85
+ title: "Webhooks & alerts",
86
+ description:
87
+ "Push events and threshold alerts straight to your endpoints.",
88
+ },
89
+ ],
90
+ apiShowcase: {
91
+ title: "Production APIs. One key.",
92
+ browseAllLabel: "Browse all APIs",
93
+ browseAllHref: "/api",
94
+ autoFromOpenApi: true,
95
+ items: [
96
+ {
97
+ name: "List resources",
98
+ path: "/v1/items",
99
+ icon: "boxes",
100
+ latency: "28ms",
101
+ },
102
+ {
103
+ name: "Create resource",
104
+ path: "/v1/items",
105
+ icon: "badge-dollar-sign",
106
+ latency: "34ms",
107
+ },
108
+ {
109
+ name: "Get resource",
110
+ path: "/v1/items/{id}",
111
+ icon: "layers",
112
+ latency: "19ms",
113
+ },
114
+ {
115
+ name: "Search",
116
+ path: "/v1/search",
117
+ icon: "arrow-left-right",
118
+ latency: "41ms",
119
+ },
120
+ {
121
+ name: "User profile",
122
+ path: "/v1/users/me",
123
+ icon: "wallet",
124
+ latency: "52ms",
125
+ },
126
+ {
127
+ name: "Health check",
128
+ path: "/v1/health",
129
+ icon: "activity",
130
+ latency: "16ms",
131
+ },
132
+ {
133
+ name: "Metadata",
134
+ path: "/v1/meta",
135
+ icon: "coins",
136
+ latency: "22ms",
137
+ },
138
+ {
139
+ name: "Rate limits",
140
+ path: "/v1/limits",
141
+ icon: "gauge",
142
+ latency: "16ms",
143
+ },
144
+ ],
145
+ },
146
+ pricingCta: {
147
+ title: "Start free. Scale when you do.",
148
+ description:
149
+ "10,000 requests a month, free forever. Upgrade only when your app takes off.",
150
+ primaryAction: { label: "Create free account", href: "/register" },
151
+ secondaryAction: { label: "See pricing", href: "/pricing" },
152
+ },
153
+ showPoweredBy: true,
154
+ };
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_LANDING_CONTENT } from "./default-landing-content.js";
1
2
  import {
2
3
  type ResolvedModulesConfig,
3
4
  ModulesSchema,
@@ -32,72 +33,137 @@ export const joinPanelPath = (basePath: string, routePath: string): string => {
32
33
  return `/${`${base}/${route}`.replace(/^\/+/, "")}`;
33
34
  };
34
35
 
35
- const DEFAULT_LANDING_FEATURES: ResolvedLandingContent["features"] = [
36
- {
37
- title: "Documentation",
38
- description:
39
- "Write guides and references in Markdown or MDX with full navigation and search.",
40
- },
41
- {
42
- title: "API Reference",
43
- description:
44
- "Publish interactive OpenAPI docs with a built-in playground and code samples.",
45
- },
46
- {
47
- title: "Developer Portal",
48
- description:
49
- "Extend with landing pages, authentication, API keys, and subscription modules.",
50
- },
51
- ];
36
+ const mergeLandingSection = <T extends { enabled?: boolean }>(
37
+ defaults: T | undefined,
38
+ override: T | undefined,
39
+ ): T | undefined => {
40
+ if (override?.enabled === false) {
41
+ return undefined;
42
+ }
52
43
 
53
- export const resolveLandingContent = (
44
+ if (!defaults && !override) {
45
+ return undefined;
46
+ }
47
+
48
+ return { ...defaults, ...override } as T;
49
+ };
50
+
51
+ const buildDefaultLandingContent = (
54
52
  config: ZudokuConfig,
55
- content?: LandingContentConfig,
56
53
  ): ResolvedLandingContent => {
57
54
  const siteTitle =
58
55
  config.metadata?.applicationName ?? config.site?.title ?? "APIToGo";
59
56
 
60
- const resolved: ResolvedLandingContent = {
57
+ return {
58
+ ...DEFAULT_LANDING_CONTENT,
61
59
  hero: {
62
- title: content?.hero?.title ?? siteTitle,
63
- subtitle: content?.hero?.subtitle ?? "Developer portal",
64
- description:
65
- content?.hero?.description ??
66
- config.metadata?.description ??
67
- "Ship beautiful API documentation and developer experiences with APIToGo.",
68
- badge: content?.hero?.badge,
69
- highlights: content?.hero?.highlights,
70
- codeExample: content?.hero?.codeExample,
60
+ ...DEFAULT_LANDING_CONTENT.hero,
61
+ title: `${siteTitle} APIs that ship`,
71
62
  },
72
- features: content?.features ?? DEFAULT_LANDING_FEATURES,
73
- primaryAction: content?.primaryAction ?? {
74
- label: "Get started",
63
+ primaryAction: {
64
+ ...DEFAULT_LANDING_CONTENT.primaryAction,
75
65
  href: "/introduction",
66
+ label: "Get started",
76
67
  },
77
- secondaryAction: content?.secondaryAction ?? {
78
- label: "API Reference",
68
+ secondaryAction: {
69
+ ...DEFAULT_LANDING_CONTENT.secondaryAction,
79
70
  href: "/api",
71
+ label: "API Reference",
80
72
  },
81
- showPoweredBy: content?.showPoweredBy,
73
+ pricingCta: DEFAULT_LANDING_CONTENT.pricingCta
74
+ ? {
75
+ ...DEFAULT_LANDING_CONTENT.pricingCta,
76
+ primaryAction: {
77
+ label: "Get started",
78
+ href: "/introduction",
79
+ },
80
+ secondaryAction: {
81
+ label: "See pricing",
82
+ href: "/pricing",
83
+ },
84
+ }
85
+ : undefined,
82
86
  };
87
+ };
83
88
 
84
- if (content?.ticker && content.ticker.enabled !== false) {
85
- resolved.ticker = content.ticker;
86
- }
89
+ export const resolveLandingContent = (
90
+ config: ZudokuConfig,
91
+ content?: LandingContentConfig,
92
+ options?: { useDefaults?: boolean },
93
+ ): ResolvedLandingContent => {
94
+ const siteTitle =
95
+ config.metadata?.applicationName ?? config.site?.title ?? "APIToGo";
87
96
 
88
- if (content?.trust && content.trust.enabled !== false) {
89
- resolved.trust = content.trust;
97
+ if (options?.useDefaults === true) {
98
+ const defaults = buildDefaultLandingContent(config);
99
+
100
+ return {
101
+ hero: {
102
+ ...defaults.hero,
103
+ ...content?.hero,
104
+ title: content?.hero?.title ?? defaults.hero?.title,
105
+ description:
106
+ content?.hero?.description ??
107
+ config.metadata?.description ??
108
+ defaults.hero?.description,
109
+ codeExample: content?.hero?.codeExample ?? defaults.hero?.codeExample,
110
+ titleAccent:
111
+ content?.hero?.titleAccent ??
112
+ (content?.hero?.title ? undefined : defaults.hero?.titleAccent),
113
+ },
114
+ featuresSection: mergeLandingSection(
115
+ defaults.featuresSection,
116
+ content?.featuresSection,
117
+ ),
118
+ features: content?.features ?? defaults.features,
119
+ primaryAction: content?.primaryAction ?? defaults.primaryAction,
120
+ secondaryAction: content?.secondaryAction ?? defaults.secondaryAction,
121
+ showPoweredBy: content?.showPoweredBy ?? defaults.showPoweredBy,
122
+ ticker: mergeLandingSection(defaults.ticker, content?.ticker),
123
+ trust: mergeLandingSection(defaults.trust, content?.trust),
124
+ apiShowcase: mergeLandingSection(
125
+ defaults.apiShowcase,
126
+ content?.apiShowcase,
127
+ ),
128
+ pricingCta: mergeLandingSection(defaults.pricingCta, content?.pricingCta),
129
+ };
90
130
  }
91
131
 
92
- if (content?.apiShowcase && content.apiShowcase.enabled !== false) {
93
- resolved.apiShowcase = content.apiShowcase;
94
- }
132
+ const hasContent =
133
+ content !== undefined && Object.keys(content).length > 0;
95
134
 
96
- if (content?.pricingCta && content.pricingCta.enabled !== false) {
97
- resolved.pricingCta = content.pricingCta;
135
+ if (!hasContent) {
136
+ return {
137
+ hero: { title: siteTitle },
138
+ showPoweredBy: true,
139
+ };
98
140
  }
99
141
 
100
- return resolved;
142
+ return {
143
+ hero: {
144
+ ...content.hero,
145
+ title: content.hero?.title ?? siteTitle,
146
+ },
147
+ featuresSection: content.featuresSection
148
+ ? mergeLandingSection(undefined, content.featuresSection)
149
+ : undefined,
150
+ features: content.features,
151
+ primaryAction: content.primaryAction,
152
+ secondaryAction: content.secondaryAction,
153
+ showPoweredBy: content.showPoweredBy ?? true,
154
+ ticker: content.ticker
155
+ ? mergeLandingSection(undefined, content.ticker)
156
+ : undefined,
157
+ trust: content.trust
158
+ ? mergeLandingSection(undefined, content.trust)
159
+ : undefined,
160
+ apiShowcase: content.apiShowcase
161
+ ? mergeLandingSection(undefined, content.apiShowcase)
162
+ : undefined,
163
+ pricingCta: content.pricingCta
164
+ ? mergeLandingSection(undefined, content.pricingCta)
165
+ : undefined,
166
+ };
101
167
  };
102
168
 
103
169
  const DEFAULT_DASHBOARD_QUICK_LINKS: ResolvedDashboardContent["quickLinks"] = [
@@ -193,7 +259,9 @@ export const resolveModulesConfig = (
193
259
  path: landingModule?.path ?? "/",
194
260
  component: landingModule?.component,
195
261
  layout: landingModule?.layout ?? "landing",
196
- content: resolveLandingContent(config, landingModule?.content),
262
+ content: resolveLandingContent(config, landingModule?.content, {
263
+ useDefaults: landingModule?.useDefaults === true,
264
+ }),
197
265
  },
198
266
  userPanel: {
199
267
  enabled: userPanelEnabled,
@@ -58,6 +58,10 @@ export const LandingCodeExampleSchema = z
58
58
  export const LandingHeroSchema = z
59
59
  .object({
60
60
  title: z.string().optional(),
61
+ titleAccent: z
62
+ .string()
63
+ .optional()
64
+ .describe("Accent-colored word or phrase shown on a new line after the title."),
61
65
  subtitle: z.string().optional(),
62
66
  description: z.string().optional(),
63
67
  badge: z.string().optional(),
@@ -66,6 +70,14 @@ export const LandingHeroSchema = z
66
70
  })
67
71
  .optional();
68
72
 
73
+ export const LandingFeaturesSectionSchema = z
74
+ .object({
75
+ label: z.string().optional(),
76
+ title: z.string().optional(),
77
+ description: z.string().optional(),
78
+ })
79
+ .optional();
80
+
69
81
  export const LandingTickerItemSchema = z.object({
70
82
  label: z.string(),
71
83
  value: z.string(),
@@ -120,6 +132,7 @@ export const LandingPricingCtaSchema = z
120
132
  export const LandingContentSchema = z
121
133
  .object({
122
134
  hero: LandingHeroSchema,
135
+ featuresSection: LandingFeaturesSectionSchema,
123
136
  features: z.array(LandingFeatureSchema).optional(),
124
137
  primaryAction: LandingLinkSchema.optional(),
125
138
  secondaryAction: LandingLinkSchema.optional(),
@@ -195,6 +208,12 @@ export const LandingModuleSchema = ModuleToggleSchema.extend({
195
208
  content: LandingContentSchema.describe(
196
209
  "Default landing page content when no custom component is provided.",
197
210
  ),
211
+ useDefaults: z
212
+ .boolean()
213
+ .optional()
214
+ .describe(
215
+ "When true, merge DEFAULT_LANDING_CONTENT for unset sections. When false or omitted, only configured sections are shown.",
216
+ ),
198
217
  });
199
218
 
200
219
  export const UserManagementModuleSchema = ModuleToggleSchema.extend({
@@ -326,12 +345,18 @@ export type LandingPricingCtaConfig = z.infer<typeof LandingPricingCtaSchema>;
326
345
  export type ResolvedLandingContent = {
327
346
  hero?: {
328
347
  title?: string;
348
+ titleAccent?: string;
329
349
  subtitle?: string;
330
350
  description?: string;
331
351
  badge?: string;
332
352
  highlights?: string[];
333
353
  codeExample?: NonNullable<LandingCodeExampleConfig>;
334
354
  };
355
+ featuresSection?: {
356
+ label?: string;
357
+ title?: string;
358
+ description?: string;
359
+ };
335
360
  features?: LandingFeatureConfig[];
336
361
  primaryAction?: LandingLinkConfig;
337
362
  secondaryAction?: LandingLinkConfig;
@@ -66,6 +66,15 @@ export function buildDevPortalOidcChallengeUrl(
66
66
  return url.toString();
67
67
  }
68
68
 
69
+ export function buildDevPortalLoginUrl(
70
+ apiBaseUrl: string,
71
+ returnUrl: string,
72
+ ): string {
73
+ const url = new URL("/api/v1/auth/login", apiBaseUrl);
74
+ url.searchParams.set("returnUrl", toAbsoluteReturnUrl(returnUrl));
75
+ return url.toString();
76
+ }
77
+
69
78
  export function buildDevPortalLogoutUrl(
70
79
  apiBaseUrl: string,
71
80
  returnUrl: string,