@lukoweb/apitogo 0.1.91 → 0.1.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +92 -45
- package/dist/declarations/cli/platform/helpers/dev-portal-manifest.d.ts +3 -0
- package/dist/declarations/config/loader.d.ts +1 -0
- package/dist/declarations/config/local-manifest.d.ts +4 -0
- package/dist/declarations/lib/components/LandingLayout.d.ts +1 -1
- package/docs/configuration/manifest.md +41 -4
- package/package.json +1 -1
- package/src/config/apitogo-config-core.ts +11 -25
- package/src/config/loader.ts +12 -0
- package/src/config/local-manifest.ts +6 -0
- package/src/lib/components/HeaderNavigation.tsx +5 -5
- package/src/lib/components/LandingLayout.tsx +21 -15
- package/src/lib/components/MobileTopNavigation.tsx +5 -4
- package/src/lib/util/useScrollToAnchor.ts +5 -0
- package/src/vite/plugin-config.ts +1 -0
- package/src/vite/plugin-modules.ts +15 -1
package/dist/cli/cli.js
CHANGED
|
@@ -301,23 +301,15 @@ function firstApiInput(config2) {
|
|
|
301
301
|
}
|
|
302
302
|
return void 0;
|
|
303
303
|
}
|
|
304
|
-
function
|
|
304
|
+
function normalizeGatewayPlacement(config2) {
|
|
305
305
|
const authentication = asRecord2(config2.authentication);
|
|
306
306
|
if (!authentication) {
|
|
307
|
-
return
|
|
307
|
+
return;
|
|
308
308
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return authentication;
|
|
309
|
+
if ("gateway" in authentication) {
|
|
310
|
+
delete authentication.gateway;
|
|
311
|
+
config2.authentication = authentication;
|
|
313
312
|
}
|
|
314
|
-
return {
|
|
315
|
-
...authentication,
|
|
316
|
-
gateway: {
|
|
317
|
-
...rootGateway,
|
|
318
|
-
...authGateway
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
313
|
}
|
|
322
314
|
function ensureModules(config2) {
|
|
323
315
|
const modules = asRecord2(config2.modules) ?? {};
|
|
@@ -378,11 +370,10 @@ function normalizeApitogoConfig(config2) {
|
|
|
378
370
|
};
|
|
379
371
|
}
|
|
380
372
|
normalized.site = site;
|
|
381
|
-
const authentication =
|
|
373
|
+
const authentication = asRecord2(normalized.authentication);
|
|
382
374
|
if (authentication) {
|
|
383
|
-
normalized
|
|
375
|
+
normalizeGatewayPlacement(normalized);
|
|
384
376
|
}
|
|
385
|
-
delete normalized.gateway;
|
|
386
377
|
const modules = ensureModules(normalized);
|
|
387
378
|
const legacyDocs = asRecord2(normalized.docs);
|
|
388
379
|
const moduleDocs = asRecord2(modules.docs) ?? {};
|
|
@@ -533,13 +524,16 @@ function deriveManifestFields(config2) {
|
|
|
533
524
|
continue;
|
|
534
525
|
}
|
|
535
526
|
if (key === "authentication") {
|
|
536
|
-
const
|
|
537
|
-
if (
|
|
538
|
-
manifest.authentication = stripAuthenticationSecrets2(
|
|
527
|
+
const authentication = asRecord2(normalized.authentication);
|
|
528
|
+
if (authentication) {
|
|
529
|
+
manifest.authentication = stripAuthenticationSecrets2(authentication);
|
|
539
530
|
}
|
|
540
531
|
continue;
|
|
541
532
|
}
|
|
542
533
|
if (key === "gateway") {
|
|
534
|
+
if (normalized.gateway !== void 0) {
|
|
535
|
+
manifest.gateway = normalized.gateway;
|
|
536
|
+
}
|
|
543
537
|
continue;
|
|
544
538
|
}
|
|
545
539
|
if (key === "pricing") {
|
|
@@ -570,10 +564,6 @@ function deriveManifestFields(config2) {
|
|
|
570
564
|
manifest[key] = normalized[key];
|
|
571
565
|
}
|
|
572
566
|
}
|
|
573
|
-
const authentication = asRecord2(normalized.authentication);
|
|
574
|
-
if (authentication?.gateway) {
|
|
575
|
-
manifest.gateway = authentication.gateway;
|
|
576
|
-
}
|
|
577
567
|
return manifest;
|
|
578
568
|
}
|
|
579
569
|
function extractManifest(config2) {
|
|
@@ -1158,7 +1148,11 @@ var init_local_manifest = __esm({
|
|
|
1158
1148
|
oidcAllowedIssuers: z4.array(z4.string()).optional(),
|
|
1159
1149
|
oidcAudiences: z4.array(z4.string()).optional(),
|
|
1160
1150
|
limitsJson: z4.string().optional(),
|
|
1161
|
-
publicHealthActionKey: z4.string().optional()
|
|
1151
|
+
publicHealthActionKey: z4.string().optional(),
|
|
1152
|
+
publicRoutes: z4.array(z4.string()).optional(),
|
|
1153
|
+
cors: z4.object({
|
|
1154
|
+
allowedOrigins: z4.array(z4.string()).optional()
|
|
1155
|
+
}).optional()
|
|
1162
1156
|
}).optional(),
|
|
1163
1157
|
projectId: z4.string().optional(),
|
|
1164
1158
|
landing: z4.object({
|
|
@@ -4878,6 +4872,13 @@ import {
|
|
|
4878
4872
|
runnerImport,
|
|
4879
4873
|
loadEnv as viteLoadEnv
|
|
4880
4874
|
} from "vite";
|
|
4875
|
+
function resolvePublishGatewayBaseUrl() {
|
|
4876
|
+
const raw = process.env.APITOGO_GATEWAY_URL?.trim();
|
|
4877
|
+
if (!raw) {
|
|
4878
|
+
return void 0;
|
|
4879
|
+
}
|
|
4880
|
+
return raw.replace(/\/+$/, "");
|
|
4881
|
+
}
|
|
4881
4882
|
function mergeJsonConfigWithRuntimeOverrides(fileConfig, importedConfig) {
|
|
4882
4883
|
const jsonConfig = buildApitogoConfig({
|
|
4883
4884
|
...fileConfig,
|
|
@@ -5037,7 +5038,8 @@ async function loadZudokuConfig(configEnv, rootDir) {
|
|
|
5037
5038
|
...loadedConfig.__meta,
|
|
5038
5039
|
pricingEnabled: isBillingEnabled(jsonConfig),
|
|
5039
5040
|
authenticationEnabled: isAuthenticationEnabled(manifest),
|
|
5040
|
-
previewPlans
|
|
5041
|
+
previewPlans,
|
|
5042
|
+
gatewayBaseUrl: resolvePublishGatewayBaseUrl()
|
|
5041
5043
|
}
|
|
5042
5044
|
};
|
|
5043
5045
|
const transformedConfig = applyAuthenticationHeaderNav(
|
|
@@ -6026,6 +6028,7 @@ function manifestFilePath(siteDirectory) {
|
|
|
6026
6028
|
var STORED_MANIFEST_KEYS = [
|
|
6027
6029
|
"authentication",
|
|
6028
6030
|
"backend",
|
|
6031
|
+
"gateway",
|
|
6029
6032
|
"projectId"
|
|
6030
6033
|
];
|
|
6031
6034
|
function readPlanItemsFromConfig(config2) {
|
|
@@ -6181,12 +6184,6 @@ function extractManifestFromConfig(config2) {
|
|
|
6181
6184
|
if (readPricingEnabledFromConfig(config2)) {
|
|
6182
6185
|
manifest.pricing = { enabled: true };
|
|
6183
6186
|
}
|
|
6184
|
-
const authConfig = config2.authentication;
|
|
6185
|
-
if (authConfig?.gateway) {
|
|
6186
|
-
manifest.gateway = authConfig.gateway;
|
|
6187
|
-
} else if (config2.gateway) {
|
|
6188
|
-
manifest.gateway = config2.gateway;
|
|
6189
|
-
}
|
|
6190
6187
|
const landing = readLandingFromConfig(config2);
|
|
6191
6188
|
if (landing) {
|
|
6192
6189
|
manifest.landing = landing;
|
|
@@ -6226,15 +6223,14 @@ function applyManifestToConfig(config2, manifest) {
|
|
|
6226
6223
|
}
|
|
6227
6224
|
delete next.plans;
|
|
6228
6225
|
delete next.pricing;
|
|
6229
|
-
if (
|
|
6230
|
-
const authentication = next.authentication
|
|
6231
|
-
authentication.gateway
|
|
6226
|
+
if (next.authentication && typeof next.authentication === "object") {
|
|
6227
|
+
const authentication = { ...next.authentication };
|
|
6228
|
+
delete authentication.gateway;
|
|
6232
6229
|
next.authentication = authentication;
|
|
6233
6230
|
}
|
|
6234
6231
|
delete next.siteName;
|
|
6235
6232
|
delete next.branding;
|
|
6236
6233
|
delete next.projectName;
|
|
6237
|
-
delete next.gateway;
|
|
6238
6234
|
return next;
|
|
6239
6235
|
}
|
|
6240
6236
|
async function readLegacyManifest(_siteDirectory) {
|
|
@@ -12537,7 +12533,7 @@ import {
|
|
|
12537
12533
|
// package.json
|
|
12538
12534
|
var package_default = {
|
|
12539
12535
|
name: "@lukoweb/apitogo",
|
|
12540
|
-
version: "0.1.
|
|
12536
|
+
version: "0.1.93",
|
|
12541
12537
|
type: "module",
|
|
12542
12538
|
sideEffects: [
|
|
12543
12539
|
"**/*.css",
|
|
@@ -15289,7 +15285,8 @@ var viteConfigPlugin = () => {
|
|
|
15289
15285
|
rootDir: loadedConfig.__meta.rootDir,
|
|
15290
15286
|
pricingEnabled: loadedConfig.__meta.pricingEnabled,
|
|
15291
15287
|
authenticationEnabled: loadedConfig.__meta.authenticationEnabled,
|
|
15292
|
-
previewPlans: loadedConfig.__meta.previewPlans
|
|
15288
|
+
previewPlans: loadedConfig.__meta.previewPlans,
|
|
15289
|
+
gatewayBaseUrl: loadedConfig.__meta.gatewayBaseUrl
|
|
15293
15290
|
};
|
|
15294
15291
|
return `
|
|
15295
15292
|
import rawConfig from "${normalizePath(configPath)}";
|
|
@@ -16252,6 +16249,9 @@ import path39 from "node:path";
|
|
|
16252
16249
|
import { normalizePath as normalizePath2 } from "vite";
|
|
16253
16250
|
var getLandingModuleImport = (moduleDir, mode) => mode === "internal" ? normalizePath2(path39.join(moduleDir, "../module-landing/src/index.tsx")) : "@lukoweb/apitogo-module-landing";
|
|
16254
16251
|
var getUserPanelModuleImport = (moduleDir, mode) => mode === "internal" ? normalizePath2(path39.join(moduleDir, "../module-user-panel/src/index.tsx")) : "@lukoweb/apitogo-module-user-panel";
|
|
16252
|
+
var getBillingPluginImport = (moduleDir, mode) => mode === "internal" ? normalizePath2(
|
|
16253
|
+
path39.join(moduleDir, "../plugin-dev-portal-billing/src/index.ts")
|
|
16254
|
+
) : "@lukoweb/apitogo-plugin-dev-portal-billing";
|
|
16255
16255
|
var resolveProjectPageImport = (rootDir, pagePath) => normalizePath2(path39.resolve(rootDir, pagePath));
|
|
16256
16256
|
var viteModulesPlugin = () => {
|
|
16257
16257
|
const virtualModuleId4 = "virtual:zudoku-modules-plugin";
|
|
@@ -16317,10 +16317,15 @@ var viteModulesPlugin = () => {
|
|
|
16317
16317
|
`import LandingPage from "${resolveProjectPageImport(config2.__meta.rootDir, landingPagePath)}";`
|
|
16318
16318
|
);
|
|
16319
16319
|
}
|
|
16320
|
+
const useBillingDashboard = !dashboardPagePath && userPanel.plans.enabled;
|
|
16320
16321
|
if (dashboardPagePath) {
|
|
16321
16322
|
code.push(
|
|
16322
16323
|
`import UserPanelDashboardPage from "${resolveProjectPageImport(config2.__meta.rootDir, dashboardPagePath)}";`
|
|
16323
16324
|
);
|
|
16325
|
+
} else if (useBillingDashboard) {
|
|
16326
|
+
code.push(
|
|
16327
|
+
`import { OverviewDashboardPage as UserPanelDashboardPage } from "${getBillingPluginImport(config2.__meta.moduleDir, config2.__meta.mode)}";`
|
|
16328
|
+
);
|
|
16324
16329
|
}
|
|
16325
16330
|
if (profilePagePath) {
|
|
16326
16331
|
code.push(
|
|
@@ -16361,7 +16366,7 @@ var viteModulesPlugin = () => {
|
|
|
16361
16366
|
` ...${JSON.stringify(userPanelConfig)},`,
|
|
16362
16367
|
` dashboard: {`,
|
|
16363
16368
|
` ...${JSON.stringify(userPanelConfig.dashboard)},`,
|
|
16364
|
-
` component: config.modules?.userPanel?.dashboard?.component${dashboardPagePath ? " ?? UserPanelDashboardPage" : ""},`,
|
|
16369
|
+
` component: config.modules?.userPanel?.dashboard?.component${dashboardPagePath || useBillingDashboard ? " ?? UserPanelDashboardPage" : ""},`,
|
|
16365
16370
|
` },`,
|
|
16366
16371
|
` userManagement: {`,
|
|
16367
16372
|
` ...${JSON.stringify(userPanelConfig.userManagement)},`,
|
|
@@ -18687,6 +18692,9 @@ function patchApitogoConfigOpenApiToFile(content, input3 = "./openapi.json") {
|
|
|
18687
18692
|
var ACCOUNT_PLANS_PAGE_PATH = "user-panel/AccountPlansPage.tsx";
|
|
18688
18693
|
var ACCOUNT_PLANS_PAGE = `export { AccountPlansPage as default } from "@lukoweb/apitogo-plugin-dev-portal-billing";
|
|
18689
18694
|
`;
|
|
18695
|
+
var DASHBOARD_PAGE_PATH = "user-panel/DashboardPage.tsx";
|
|
18696
|
+
var DASHBOARD_PAGE = `export { OverviewDashboardPage as default } from "@lukoweb/apitogo-plugin-dev-portal-billing";
|
|
18697
|
+
`;
|
|
18690
18698
|
function patchAccountPlansPageConfig(content) {
|
|
18691
18699
|
if (/plans:\s*\{[^}]*page:/s.test(content)) {
|
|
18692
18700
|
return content;
|
|
@@ -18699,6 +18707,18 @@ function patchAccountPlansPageConfig(content) {
|
|
|
18699
18707
|
}`
|
|
18700
18708
|
);
|
|
18701
18709
|
}
|
|
18710
|
+
function patchDashboardPageConfig(content) {
|
|
18711
|
+
if (/dashboard:\s*\{[^}]*page:/s.test(content)) {
|
|
18712
|
+
return content;
|
|
18713
|
+
}
|
|
18714
|
+
return content.replace(
|
|
18715
|
+
/dashboard:\s*\{\s*enabled:\s*true\s*\}/,
|
|
18716
|
+
`dashboard: {
|
|
18717
|
+
enabled: true,
|
|
18718
|
+
page: "./user-panel/DashboardPage.tsx",
|
|
18719
|
+
}`
|
|
18720
|
+
);
|
|
18721
|
+
}
|
|
18702
18722
|
async function ensureAccountPlansPage(outputDirectory) {
|
|
18703
18723
|
const pagePath = path53.join(outputDirectory, ACCOUNT_PLANS_PAGE_PATH);
|
|
18704
18724
|
try {
|
|
@@ -18710,6 +18730,17 @@ async function ensureAccountPlansPage(outputDirectory) {
|
|
|
18710
18730
|
return ACCOUNT_PLANS_PAGE_PATH;
|
|
18711
18731
|
}
|
|
18712
18732
|
}
|
|
18733
|
+
async function ensureDashboardPage(outputDirectory) {
|
|
18734
|
+
const pagePath = path53.join(outputDirectory, DASHBOARD_PAGE_PATH);
|
|
18735
|
+
try {
|
|
18736
|
+
await readFile13(pagePath, "utf8");
|
|
18737
|
+
return void 0;
|
|
18738
|
+
} catch {
|
|
18739
|
+
await mkdir10(path53.dirname(pagePath), { recursive: true });
|
|
18740
|
+
await writeFile12(pagePath, DASHBOARD_PAGE);
|
|
18741
|
+
return DASHBOARD_PAGE_PATH;
|
|
18742
|
+
}
|
|
18743
|
+
}
|
|
18713
18744
|
function patchApitogoConfig(content, options) {
|
|
18714
18745
|
let patched = content;
|
|
18715
18746
|
if (options.title) {
|
|
@@ -18773,12 +18804,14 @@ async function patchScaffoldedSite(outputDirectory, options) {
|
|
|
18773
18804
|
const configContent = await readFile13(configPath, "utf8");
|
|
18774
18805
|
await writeFile12(
|
|
18775
18806
|
configPath,
|
|
18776
|
-
|
|
18777
|
-
|
|
18778
|
-
|
|
18779
|
-
|
|
18780
|
-
|
|
18781
|
-
|
|
18807
|
+
patchDashboardPageConfig(
|
|
18808
|
+
patchAccountPlansPageConfig(
|
|
18809
|
+
patchApitogoConfig(configContent, {
|
|
18810
|
+
title,
|
|
18811
|
+
openApiUrl: options.openApiUrl,
|
|
18812
|
+
logoUrl: options.branding?.logoUrl
|
|
18813
|
+
})
|
|
18814
|
+
)
|
|
18782
18815
|
)
|
|
18783
18816
|
);
|
|
18784
18817
|
filesWritten.push("apitogo.config.tsx");
|
|
@@ -18786,6 +18819,10 @@ async function patchScaffoldedSite(outputDirectory, options) {
|
|
|
18786
18819
|
if (accountPlansPage) {
|
|
18787
18820
|
filesWritten.push(accountPlansPage);
|
|
18788
18821
|
}
|
|
18822
|
+
const dashboardPage = await ensureDashboardPage(outputDirectory);
|
|
18823
|
+
if (dashboardPage) {
|
|
18824
|
+
filesWritten.push(dashboardPage);
|
|
18825
|
+
}
|
|
18789
18826
|
return filesWritten;
|
|
18790
18827
|
}
|
|
18791
18828
|
async function scaffoldDevPortalSite(options) {
|
|
@@ -19138,7 +19175,10 @@ function createDefaultApitogoJsonConfig(options) {
|
|
|
19138
19175
|
},
|
|
19139
19176
|
userPanel: {
|
|
19140
19177
|
enabled: true,
|
|
19141
|
-
dashboard: {
|
|
19178
|
+
dashboard: {
|
|
19179
|
+
enabled: true,
|
|
19180
|
+
page: "./user-panel/DashboardPage.tsx"
|
|
19181
|
+
},
|
|
19142
19182
|
userManagement: { enabled: true },
|
|
19143
19183
|
plans: {
|
|
19144
19184
|
enabled: true,
|
|
@@ -19963,6 +20003,8 @@ async function ensureTsconfigJson(dir) {
|
|
|
19963
20003
|
var LANDING_PAGE_SOURCE = 'import {\n ApiShowcaseSection,\n FeaturesSection,\n HeroSection,\n LandingShell,\n PlansPreviewSection,\n PricingCtaSection,\n StepsSection,\n} from "@lukoweb/apitogo-module-landing";\n\nexport default function LandingPage() {\n return (\n <LandingShell\n title="Test against a real API before you build one"\n description="Sample API gives you realistic endpoints, authentication, rate limits, and webhooks \u2014 a fully working backend for prototypes, demos, and QA."\n >\n <HeroSection\n hero={{\n badge: "SANDBOX API",\n title: "Test against a real API before you build one",\n description:\n "Sample API gives you realistic endpoints, authentication, rate limits, and webhooks \u2014 a fully working backend for prototypes, demos, and QA. No setup, no infrastructure.",\n }}\n primaryAction={{ label: "Start for free", href: "/signup" }}\n secondaryAction={{ label: "Read the docs", href: "/introduction" }}\n codeExamples={[\n {\n filename: "Create a key",\n language: "bash",\n code: `$ curl -X POST https://api.sampleapi.dev/v1/keys\n# sk_live_9f2c7a1e...`,\n },\n {\n filename: "Call an endpoint",\n language: "bash",\n code: `$ curl https://api.sampleapi.dev/v1/users/42 \\\\\n -H "Authorization: Bearer sk_live_9f2c..."\n# { "id": 42, "name": "Priya Shah", "plan": "pro" }`,\n },\n ]}\n />\n <FeaturesSection\n section={{\n label: "Everything a real API has",\n title: "Built for testing, not just browsing",\n description:\n "Realistic data, live auth, rate limits, and webhooks \u2014 so you can ship with confidence.",\n }}\n features={[\n {\n title: "Realistic mock data",\n description:\n "Users, orders, products, and posts with relationships and pagination, not lorem ipsum.",\n icon: "boxes",\n },\n {\n title: "Real auth flow",\n description:\n "Every request checks a live API key, so you can test error handling and expiry.",\n icon: "shield-check",\n },\n {\n title: "Rate limiting",\n description:\n "Per-plan request limits and 429 responses, so you can build and test backoff logic.",\n icon: "gauge",\n },\n {\n title: "Webhooks",\n description:\n "Trigger test events and inspect delivered payloads and retries.",\n icon: "webhook",\n },\n {\n title: "OpenAPI & docs",\n description:\n "A full OpenAPI spec and reference docs for every endpoint.",\n icon: "book-open",\n },\n {\n title: "Usage analytics",\n description:\n "Track requests, latency, and errors per key from your dashboard.",\n icon: "activity",\n },\n ]}\n />\n <StepsSection\n steps={[\n {\n step: "01",\n title: "Create an account",\n description:\n "Sign up and land straight in your dashboard \u2014 no approval queue.",\n },\n {\n step: "02",\n title: "Generate a key",\n description:\n "Every key is scoped and rate-limited to your plan from the moment it\'s created.",\n },\n {\n step: "03",\n title: "Call any endpoint",\n description:\n "Users, orders, products, posts, and webhooks \u2014 all documented, all live.",\n },\n ]}\n />\n <ApiShowcaseSection\n apiShowcase={{\n title: "Sample endpoints",\n subtitle:\n "A slice of what\'s available \u2014 see the full reference in the docs.",\n browseAllHref: "/api",\n browseAllLabel: "Browse all APIs",\n layout: "list",\n items: [\n {\n name: "List paginated users",\n path: "/v1/users",\n method: "GET",\n summary: "List paginated users",\n },\n {\n name: "Create a mock order",\n path: "/v1/orders",\n method: "POST",\n summary: "Create a mock order",\n },\n {\n name: "Fetch one product",\n path: "/v1/products/{id}",\n method: "GET",\n summary: "Fetch one product",\n },\n {\n name: "List posts with comments",\n path: "/v1/posts",\n method: "GET",\n summary: "List posts with comments",\n },\n {\n name: "Fire a test webhook event",\n path: "/v1/webhooks/test",\n method: "POST",\n summary: "Fire a test webhook event",\n },\n {\n name: "Delete a mock order",\n path: "/v1/orders/{id}",\n method: "DELETE",\n summary: "Delete a mock order",\n },\n ],\n }}\n />\n <PlansPreviewSection\n plans={[\n {\n name: "Free",\n price: "$0",\n unit: "per month",\n blurb: "1,000 requests/mo, community support.",\n },\n {\n name: "Developer",\n price: "$19",\n unit: "per month",\n blurb: "50,000 requests/mo, webhooks included.",\n featured: true,\n },\n {\n name: "Team",\n price: "$79",\n unit: "per month",\n blurb: "500,000 requests/mo, 5 team seats.",\n },\n {\n name: "Enterprise",\n price: "Custom",\n unit: "talk to us",\n blurb: "Unlimited requests, SSO, SLA.",\n },\n ]}\n />\n <PricingCtaSection\n variant="banner"\n pricingCta={{\n title: "Start testing in minutes",\n description:\n "Free tier includes 1,000 requests a month \u2014 no card required.",\n primaryAction: { label: "Start for free", href: "/signup" },\n }}\n />\n </LandingShell>\n );\n}\n';
|
|
19964
20004
|
var ACCOUNT_PLANS_PAGE_SOURCE = `export { AccountPlansPage as default } from "@lukoweb/apitogo-plugin-dev-portal-billing";
|
|
19965
20005
|
`;
|
|
20006
|
+
var DASHBOARD_PAGE_SOURCE = `export { OverviewDashboardPage as default } from "@lukoweb/apitogo-plugin-dev-portal-billing";
|
|
20007
|
+
`;
|
|
19966
20008
|
var fileExists2 = async (filePath) => {
|
|
19967
20009
|
try {
|
|
19968
20010
|
await access7(filePath, constants.F_OK);
|
|
@@ -19982,6 +20024,11 @@ async function ensurePortalStubFiles(dir) {
|
|
|
19982
20024
|
await mkdir11(path57.join(dir, "user-panel"), { recursive: true });
|
|
19983
20025
|
await writeFile14(plansPath, ACCOUNT_PLANS_PAGE_SOURCE, "utf8");
|
|
19984
20026
|
}
|
|
20027
|
+
const dashboardPath = path57.join(dir, "user-panel", "DashboardPage.tsx");
|
|
20028
|
+
if (!await fileExists2(dashboardPath)) {
|
|
20029
|
+
await mkdir11(path57.join(dir, "user-panel"), { recursive: true });
|
|
20030
|
+
await writeFile14(dashboardPath, DASHBOARD_PAGE_SOURCE, "utf8");
|
|
20031
|
+
}
|
|
19985
20032
|
await ensurePublicBrandingFiles(dir);
|
|
19986
20033
|
}
|
|
19987
20034
|
async function ensurePublicBrandingFiles(dir) {
|
|
@@ -89,6 +89,10 @@ export declare const DevPortalLocalManifestSchema: z.ZodObject<{
|
|
|
89
89
|
oidcAudiences: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
90
90
|
limitsJson: z.ZodOptional<z.ZodString>;
|
|
91
91
|
publicHealthActionKey: z.ZodOptional<z.ZodString>;
|
|
92
|
+
publicRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
93
|
+
cors: z.ZodOptional<z.ZodObject<{
|
|
94
|
+
allowedOrigins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
95
|
+
}, z.core.$strip>>;
|
|
92
96
|
}, z.core.$strip>>;
|
|
93
97
|
projectId: z.ZodOptional<z.ZodString>;
|
|
94
98
|
landing: z.ZodOptional<z.ZodObject<{
|
|
@@ -25,10 +25,17 @@ metadata, with optional `.env` overrides for secrets and local tweaks.
|
|
|
25
25
|
"authentication": {
|
|
26
26
|
"enabled": true,
|
|
27
27
|
"type": "dev-portal",
|
|
28
|
-
"apiBaseUrl": "https://your-dev-portal-api.example.com"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
"apiBaseUrl": "https://your-dev-portal-api.example.com"
|
|
29
|
+
},
|
|
30
|
+
"gateway": {
|
|
31
|
+
"authMode": "bringYourOwnOidc",
|
|
32
|
+
"oidcMetadataUrl": "https://login.example.com/.well-known/openid-configuration",
|
|
33
|
+
"publicRoutes": ["/health"],
|
|
34
|
+
"cors": {
|
|
35
|
+
"allowedOrigins": [
|
|
36
|
+
"https://myapp.example.com",
|
|
37
|
+
"http://localhost:5173"
|
|
38
|
+
]
|
|
32
39
|
}
|
|
33
40
|
},
|
|
34
41
|
"modules": {
|
|
@@ -60,6 +67,36 @@ metadata, with optional `.env` overrides for secrets and local tweaks.
|
|
|
60
67
|
`modules.userPanel.plans.items`. Add plans with `apitogo config` or edit the manifest by hand.
|
|
61
68
|
See [Gateway plans](./plans.md). Legacy root `plans` is migrated on load.
|
|
62
69
|
- **Docs / API keys:** configure under `modules.docs` and `modules.userPanel.apiKeys` only.
|
|
70
|
+
- **Gateway:** top-level `gateway` (not under `authentication`). See [Gateway CORS](#gateway-cors)
|
|
71
|
+
and [Gateway plans](./plans.md).
|
|
72
|
+
|
|
73
|
+
## Gateway CORS
|
|
74
|
+
|
|
75
|
+
Browser calls from your developer portal (and any other SPA) to the API gateway are cross-origin.
|
|
76
|
+
APItoGo always allows the portal site origin. Add extra origins under `gateway.cors.allowedOrigins`
|
|
77
|
+
when tenant apps or local frontends call the gateway:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"gateway": {
|
|
82
|
+
"cors": {
|
|
83
|
+
"allowedOrigins": [
|
|
84
|
+
"https://myapp.example.com",
|
|
85
|
+
"http://localhost:5173"
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Rule | Detail |
|
|
93
|
+
| ---- | ------ |
|
|
94
|
+
| Portal origin | Always allowed by the platform (`https://{your-portal-host}`). Do not list it here. |
|
|
95
|
+
| Extra origins | Absolute `http://` or `https://` origins only — no path, query, or fragment. |
|
|
96
|
+
| Wildcard | `"*"` is allowed (replaces the list; credentials stay off). |
|
|
97
|
+
| Empty / omitted | Portal playground still works; only extras are missing. |
|
|
98
|
+
|
|
99
|
+
Apply with `apitogo apply` / `publish_dev_portal` so APIM policies pick up the change.
|
|
63
100
|
|
|
64
101
|
## Environment variables
|
|
65
102
|
|
package/package.json
CHANGED
|
@@ -297,28 +297,17 @@ function firstApiInput(config: ApitogoJsonConfig): string | undefined {
|
|
|
297
297
|
return undefined;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
): Record<string, unknown> | undefined {
|
|
300
|
+
/** Keep gateway at config root; strip any nested authentication.gateway. */
|
|
301
|
+
function normalizeGatewayPlacement(config: ApitogoJsonConfig): void {
|
|
303
302
|
const authentication = asRecord(config.authentication);
|
|
304
303
|
if (!authentication) {
|
|
305
|
-
return
|
|
304
|
+
return;
|
|
306
305
|
}
|
|
307
306
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (!authGateway && !rootGateway) {
|
|
312
|
-
return authentication;
|
|
307
|
+
if ("gateway" in authentication) {
|
|
308
|
+
delete authentication.gateway;
|
|
309
|
+
config.authentication = authentication;
|
|
313
310
|
}
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
...authentication,
|
|
317
|
-
gateway: {
|
|
318
|
-
...rootGateway,
|
|
319
|
-
...authGateway,
|
|
320
|
-
},
|
|
321
|
-
};
|
|
322
311
|
}
|
|
323
312
|
|
|
324
313
|
function ensureModules(config: ApitogoJsonConfig): Record<string, unknown> {
|
|
@@ -403,11 +392,10 @@ export function normalizeApitogoConfig(
|
|
|
403
392
|
}
|
|
404
393
|
normalized.site = site;
|
|
405
394
|
|
|
406
|
-
const authentication =
|
|
395
|
+
const authentication = asRecord(normalized.authentication);
|
|
407
396
|
if (authentication) {
|
|
408
|
-
normalized
|
|
397
|
+
normalizeGatewayPlacement(normalized);
|
|
409
398
|
}
|
|
410
|
-
delete normalized.gateway;
|
|
411
399
|
|
|
412
400
|
const modules = ensureModules(normalized);
|
|
413
401
|
const legacyDocs = asRecord(normalized.docs);
|
|
@@ -654,6 +642,9 @@ export function deriveManifestFields(
|
|
|
654
642
|
continue;
|
|
655
643
|
}
|
|
656
644
|
if (key === "gateway") {
|
|
645
|
+
if (normalized.gateway !== undefined) {
|
|
646
|
+
manifest.gateway = normalized.gateway;
|
|
647
|
+
}
|
|
657
648
|
continue;
|
|
658
649
|
}
|
|
659
650
|
if (key === "pricing") {
|
|
@@ -686,11 +677,6 @@ export function deriveManifestFields(
|
|
|
686
677
|
}
|
|
687
678
|
}
|
|
688
679
|
|
|
689
|
-
const authentication = asRecord(normalized.authentication);
|
|
690
|
-
if (authentication?.gateway) {
|
|
691
|
-
manifest.gateway = authentication.gateway;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
680
|
return manifest;
|
|
695
681
|
}
|
|
696
682
|
|
package/src/config/loader.ts
CHANGED
|
@@ -45,6 +45,8 @@ export type ConfigWithMeta = ZudokuConfig & {
|
|
|
45
45
|
pricingEnabled?: boolean;
|
|
46
46
|
authenticationEnabled?: boolean;
|
|
47
47
|
previewPlans?: DevPortalPreviewPlan[];
|
|
48
|
+
/** Live gateway URL baked in at publish/build (`APITOGO_GATEWAY_URL`). */
|
|
49
|
+
gatewayBaseUrl?: string;
|
|
48
50
|
};
|
|
49
51
|
__resolvedModules: ResolvedModulesConfig;
|
|
50
52
|
};
|
|
@@ -69,6 +71,15 @@ const zudokuConfigFiles = [
|
|
|
69
71
|
|
|
70
72
|
const serverConfigOutputFiles = ["apitogo.config.js", "zudoku.config.js"];
|
|
71
73
|
|
|
74
|
+
function resolvePublishGatewayBaseUrl(): string | undefined {
|
|
75
|
+
const raw = process.env.APITOGO_GATEWAY_URL?.trim();
|
|
76
|
+
if (!raw) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return raw.replace(/\/+$/, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
export function mergeJsonConfigWithRuntimeOverrides(
|
|
73
84
|
fileConfig: Record<string, unknown>,
|
|
74
85
|
importedConfig: ZudokuConfig,
|
|
@@ -304,6 +315,7 @@ export async function loadZudokuConfig(
|
|
|
304
315
|
pricingEnabled: isBillingEnabled(jsonConfig),
|
|
305
316
|
authenticationEnabled: isAuthenticationEnabled(manifest),
|
|
306
317
|
previewPlans,
|
|
318
|
+
gatewayBaseUrl: resolvePublishGatewayBaseUrl(),
|
|
307
319
|
},
|
|
308
320
|
};
|
|
309
321
|
const transformedConfig = applyAuthenticationHeaderNav(
|
|
@@ -140,6 +140,12 @@ export const DevPortalLocalManifestSchema = z.object({
|
|
|
140
140
|
oidcAudiences: z.array(z.string()).optional(),
|
|
141
141
|
limitsJson: z.string().optional(),
|
|
142
142
|
publicHealthActionKey: z.string().optional(),
|
|
143
|
+
publicRoutes: z.array(z.string()).optional(),
|
|
144
|
+
cors: z
|
|
145
|
+
.object({
|
|
146
|
+
allowedOrigins: z.array(z.string()).optional(),
|
|
147
|
+
})
|
|
148
|
+
.optional(),
|
|
143
149
|
})
|
|
144
150
|
.optional(),
|
|
145
151
|
projectId: z.string().optional(),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { LucideIcon } from "lucide-react";
|
|
2
|
-
import { Link } from "react-router";
|
|
3
2
|
import {
|
|
4
3
|
isHeaderNavGroup,
|
|
5
4
|
type HeaderNavGroup,
|
|
@@ -18,6 +17,7 @@ import {
|
|
|
18
17
|
NavigationMenuTrigger,
|
|
19
18
|
} from "../ui/NavigationMenu.js";
|
|
20
19
|
import { cn } from "../util/cn.js";
|
|
20
|
+
import { AnchorLink } from "./AnchorLink.js";
|
|
21
21
|
import { useZudoku } from "./context/ZudokuContext.js";
|
|
22
22
|
import { shouldShowItem } from "./navigation/utils.js";
|
|
23
23
|
|
|
@@ -28,7 +28,7 @@ const NavLinkItem = ({ item }: { item: HeaderNavLinkItem }) => {
|
|
|
28
28
|
const Icon = item.icon as LucideIcon | undefined;
|
|
29
29
|
return (
|
|
30
30
|
<NavigationMenuLink asChild>
|
|
31
|
-
<
|
|
31
|
+
<AnchorLink
|
|
32
32
|
to={item.to}
|
|
33
33
|
target={item.target}
|
|
34
34
|
rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
|
|
@@ -45,7 +45,7 @@ const NavLinkItem = ({ item }: { item: HeaderNavLinkItem }) => {
|
|
|
45
45
|
{item.description}
|
|
46
46
|
</p>
|
|
47
47
|
)}
|
|
48
|
-
</
|
|
48
|
+
</AnchorLink>
|
|
49
49
|
</NavigationMenuLink>
|
|
50
50
|
);
|
|
51
51
|
};
|
|
@@ -117,7 +117,7 @@ const HeaderNavItemComponent = ({
|
|
|
117
117
|
return (
|
|
118
118
|
<NavigationMenuItem>
|
|
119
119
|
<NavigationMenuLink asChild>
|
|
120
|
-
<
|
|
120
|
+
<AnchorLink
|
|
121
121
|
to={item.to}
|
|
122
122
|
target={item.target}
|
|
123
123
|
rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
|
|
@@ -125,7 +125,7 @@ const HeaderNavItemComponent = ({
|
|
|
125
125
|
>
|
|
126
126
|
{Icon && <Icon size={16} />}
|
|
127
127
|
{item.label}
|
|
128
|
-
</
|
|
128
|
+
</AnchorLink>
|
|
129
129
|
</NavigationMenuLink>
|
|
130
130
|
</NavigationMenuItem>
|
|
131
131
|
);
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { TooltipProvider } from "@lukoweb/apitogo/ui/Tooltip.js";
|
|
2
|
-
import type
|
|
3
|
-
import { Suspense } from "react";
|
|
2
|
+
import { type ReactNode, Suspense } from "react";
|
|
4
3
|
import { Outlet } from "react-router";
|
|
4
|
+
import { useScrollToAnchor } from "../util/useScrollToAnchor.js";
|
|
5
|
+
import { useScrollToTop } from "../util/useScrollToTop.js";
|
|
5
6
|
import { Footer } from "./Footer.js";
|
|
6
7
|
import { Header } from "./Header.js";
|
|
7
8
|
import { Slot } from "./Slot.js";
|
|
8
9
|
|
|
9
|
-
export const LandingLayout = ({ children }: { children?: ReactNode }) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
export const LandingLayout = ({ children }: { children?: ReactNode }) => {
|
|
11
|
+
useScrollToAnchor();
|
|
12
|
+
useScrollToTop();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<TooltipProvider>
|
|
16
|
+
<Slot.Target name="layout-before-head" />
|
|
17
|
+
<Suspense>
|
|
18
|
+
<Header />
|
|
19
|
+
</Suspense>
|
|
20
|
+
<Slot.Target name="layout-after-head" />
|
|
21
|
+
<main className="min-h-[calc(100dvh-var(--top-header-height))] w-full">
|
|
22
|
+
{children ?? <Outlet />}
|
|
23
|
+
</main>
|
|
24
|
+
<Footer />
|
|
25
|
+
</TooltipProvider>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
DrawerTitle,
|
|
29
29
|
DrawerTrigger,
|
|
30
30
|
} from "../ui/Drawer.js";
|
|
31
|
+
import { AnchorLink } from "./AnchorLink.js";
|
|
31
32
|
import { useCurrentNavigation, useZudoku } from "./context/ZudokuContext.js";
|
|
32
33
|
import { PoweredByZudoku } from "./navigation/PoweredByZudoku.js";
|
|
33
34
|
import { getFirstMatchingPath, shouldShowItem } from "./navigation/utils.js";
|
|
@@ -44,7 +45,7 @@ const MobileHeaderNavLink = ({
|
|
|
44
45
|
}) => {
|
|
45
46
|
const Icon = item.icon as LucideIcon | undefined;
|
|
46
47
|
return (
|
|
47
|
-
<
|
|
48
|
+
<AnchorLink
|
|
48
49
|
to={item.to}
|
|
49
50
|
target={item.target}
|
|
50
51
|
rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
|
|
@@ -53,7 +54,7 @@ const MobileHeaderNavLink = ({
|
|
|
53
54
|
>
|
|
54
55
|
{Icon && <Icon size={16} />}
|
|
55
56
|
{item.label}
|
|
56
|
-
</
|
|
57
|
+
</AnchorLink>
|
|
57
58
|
);
|
|
58
59
|
};
|
|
59
60
|
|
|
@@ -68,7 +69,7 @@ const MobileHeaderNavItem = ({
|
|
|
68
69
|
const Icon = item.icon as LucideIcon | undefined;
|
|
69
70
|
return (
|
|
70
71
|
<li className="w-full">
|
|
71
|
-
<
|
|
72
|
+
<AnchorLink
|
|
72
73
|
to={item.to}
|
|
73
74
|
target={item.target}
|
|
74
75
|
rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
|
|
@@ -77,7 +78,7 @@ const MobileHeaderNavItem = ({
|
|
|
77
78
|
>
|
|
78
79
|
{Icon && <Icon size={16} />}
|
|
79
80
|
{item.label}
|
|
80
|
-
</
|
|
81
|
+
</AnchorLink>
|
|
81
82
|
</li>
|
|
82
83
|
);
|
|
83
84
|
}
|
|
@@ -57,6 +57,11 @@ export const useScrollToAnchor = () => {
|
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
if (scrollToHash(location.hash)) {
|
|
61
|
+
initialScrolled.current = true;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
60
65
|
const observer = new MutationObserver((_, obs) => {
|
|
61
66
|
if (!scrollToHash(location.hash)) return;
|
|
62
67
|
initialScrolled.current = true;
|
|
@@ -69,6 +69,7 @@ const viteConfigPlugin = (): Plugin => {
|
|
|
69
69
|
pricingEnabled: loadedConfig.__meta.pricingEnabled,
|
|
70
70
|
authenticationEnabled: loadedConfig.__meta.authenticationEnabled,
|
|
71
71
|
previewPlans: loadedConfig.__meta.previewPlans,
|
|
72
|
+
gatewayBaseUrl: loadedConfig.__meta.gatewayBaseUrl,
|
|
72
73
|
};
|
|
73
74
|
|
|
74
75
|
return `
|
|
@@ -14,6 +14,13 @@ const getUserPanelModuleImport = (moduleDir: string, mode: string) =>
|
|
|
14
14
|
? normalizePath(path.join(moduleDir, "../module-user-panel/src/index.tsx"))
|
|
15
15
|
: "@lukoweb/apitogo-module-user-panel";
|
|
16
16
|
|
|
17
|
+
const getBillingPluginImport = (moduleDir: string, mode: string) =>
|
|
18
|
+
mode === "internal"
|
|
19
|
+
? normalizePath(
|
|
20
|
+
path.join(moduleDir, "../plugin-dev-portal-billing/src/index.ts"),
|
|
21
|
+
)
|
|
22
|
+
: "@lukoweb/apitogo-plugin-dev-portal-billing";
|
|
23
|
+
|
|
17
24
|
const resolveProjectPageImport = (rootDir: string, pagePath: string) =>
|
|
18
25
|
normalizePath(path.resolve(rootDir, pagePath));
|
|
19
26
|
|
|
@@ -91,10 +98,17 @@ const viteModulesPlugin = (): Plugin => {
|
|
|
91
98
|
);
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
const useBillingDashboard =
|
|
102
|
+
!dashboardPagePath && userPanel.plans.enabled;
|
|
103
|
+
|
|
94
104
|
if (dashboardPagePath) {
|
|
95
105
|
code.push(
|
|
96
106
|
`import UserPanelDashboardPage from "${resolveProjectPageImport(config.__meta.rootDir, dashboardPagePath)}";`,
|
|
97
107
|
);
|
|
108
|
+
} else if (useBillingDashboard) {
|
|
109
|
+
code.push(
|
|
110
|
+
`import { OverviewDashboardPage as UserPanelDashboardPage } from "${getBillingPluginImport(config.__meta.moduleDir, config.__meta.mode)}";`,
|
|
111
|
+
);
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
if (profilePagePath) {
|
|
@@ -146,7 +160,7 @@ const viteModulesPlugin = (): Plugin => {
|
|
|
146
160
|
` ...${JSON.stringify(userPanelConfig)},`,
|
|
147
161
|
` dashboard: {`,
|
|
148
162
|
` ...${JSON.stringify(userPanelConfig.dashboard)},`,
|
|
149
|
-
` component: config.modules?.userPanel?.dashboard?.component${dashboardPagePath ? " ?? UserPanelDashboardPage" : ""},`,
|
|
163
|
+
` component: config.modules?.userPanel?.dashboard?.component${dashboardPagePath || useBillingDashboard ? " ?? UserPanelDashboardPage" : ""},`,
|
|
150
164
|
` },`,
|
|
151
165
|
` userManagement: {`,
|
|
152
166
|
` ...${JSON.stringify(userPanelConfig.userManagement)},`,
|