@lukoweb/apitogo 0.1.127 → 0.1.130
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
CHANGED
|
@@ -836,7 +836,7 @@ import { stat } from "node:fs/promises";
|
|
|
836
836
|
var fileExists;
|
|
837
837
|
var init_file_exists = __esm({
|
|
838
838
|
"src/config/file-exists.ts"() {
|
|
839
|
-
fileExists = (
|
|
839
|
+
fileExists = (path73) => stat(path73).then(() => true).catch(() => false);
|
|
840
840
|
}
|
|
841
841
|
});
|
|
842
842
|
|
|
@@ -5809,7 +5809,7 @@ import {
|
|
|
5809
5809
|
// package.json
|
|
5810
5810
|
var package_default = {
|
|
5811
5811
|
name: "@lukoweb/apitogo",
|
|
5812
|
-
version: "0.1.
|
|
5812
|
+
version: "0.1.130",
|
|
5813
5813
|
type: "module",
|
|
5814
5814
|
sideEffects: [
|
|
5815
5815
|
"**/*.css",
|
|
@@ -6588,14 +6588,14 @@ var flattenAllOf = (schema2) => {
|
|
|
6588
6588
|
};
|
|
6589
6589
|
|
|
6590
6590
|
// src/lib/util/traverse.ts
|
|
6591
|
-
var traverse = (specification, transform,
|
|
6592
|
-
const transformed = transform(specification,
|
|
6591
|
+
var traverse = (specification, transform, path73 = []) => {
|
|
6592
|
+
const transformed = transform(specification, path73);
|
|
6593
6593
|
if (typeof transformed !== "object" || transformed === null) {
|
|
6594
6594
|
return transformed;
|
|
6595
6595
|
}
|
|
6596
6596
|
const result = Array.isArray(transformed) ? [] : {};
|
|
6597
6597
|
for (const [key, value] of Object.entries(transformed)) {
|
|
6598
|
-
const currentPath = [...
|
|
6598
|
+
const currentPath = [...path73, key];
|
|
6599
6599
|
if (Array.isArray(value)) {
|
|
6600
6600
|
result[key] = value.map(
|
|
6601
6601
|
(item, index) => typeof item === "object" && item != null ? traverse(item, transform, [...currentPath, index.toString()]) : item
|
|
@@ -6623,9 +6623,9 @@ var resolveLocalRef = (schema2, ref) => {
|
|
|
6623
6623
|
if (schemaCache?.has(ref)) {
|
|
6624
6624
|
return schemaCache.get(ref);
|
|
6625
6625
|
}
|
|
6626
|
-
const
|
|
6626
|
+
const path73 = ref.split("/").slice(1);
|
|
6627
6627
|
let current = schema2;
|
|
6628
|
-
for (const segment of
|
|
6628
|
+
for (const segment of path73) {
|
|
6629
6629
|
if (!current || typeof current !== "object") {
|
|
6630
6630
|
current = null;
|
|
6631
6631
|
}
|
|
@@ -6644,7 +6644,7 @@ var dereference = async (schema2, resolvers = []) => {
|
|
|
6644
6644
|
}
|
|
6645
6645
|
const cloned = structuredClone(schema2);
|
|
6646
6646
|
const visited = /* @__PURE__ */ new Set();
|
|
6647
|
-
const resolve = async (current,
|
|
6647
|
+
const resolve = async (current, path73) => {
|
|
6648
6648
|
if (isIndexableObject(current)) {
|
|
6649
6649
|
if (visited.has(current)) {
|
|
6650
6650
|
return CIRCULAR_REF;
|
|
@@ -6652,7 +6652,7 @@ var dereference = async (schema2, resolvers = []) => {
|
|
|
6652
6652
|
visited.add(current);
|
|
6653
6653
|
if (Array.isArray(current)) {
|
|
6654
6654
|
for (let index = 0; index < current.length; index++) {
|
|
6655
|
-
current[index] = await resolve(current[index], `${
|
|
6655
|
+
current[index] = await resolve(current[index], `${path73}/${index}`);
|
|
6656
6656
|
}
|
|
6657
6657
|
} else {
|
|
6658
6658
|
if ("$ref" in current && typeof current.$ref === "string") {
|
|
@@ -6663,13 +6663,13 @@ var dereference = async (schema2, resolvers = []) => {
|
|
|
6663
6663
|
for (const resolver of resolvers) {
|
|
6664
6664
|
const resolved = await resolver($ref);
|
|
6665
6665
|
if (resolved) {
|
|
6666
|
-
result2 = await resolve(resolved,
|
|
6666
|
+
result2 = await resolve(resolved, path73);
|
|
6667
6667
|
break;
|
|
6668
6668
|
}
|
|
6669
6669
|
}
|
|
6670
6670
|
if (result2 === void 0) {
|
|
6671
6671
|
const resolved = await resolveLocalRef(cloned, $ref);
|
|
6672
|
-
result2 = await resolve(resolved,
|
|
6672
|
+
result2 = await resolve(resolved, path73);
|
|
6673
6673
|
}
|
|
6674
6674
|
if (hasSiblings) {
|
|
6675
6675
|
if (result2 === CIRCULAR_REF) {
|
|
@@ -6682,7 +6682,7 @@ var dereference = async (schema2, resolvers = []) => {
|
|
|
6682
6682
|
return result2;
|
|
6683
6683
|
}
|
|
6684
6684
|
for (const key in current) {
|
|
6685
|
-
current[key] = await resolve(current[key], `${
|
|
6685
|
+
current[key] = await resolve(current[key], `${path73}/${key}`);
|
|
6686
6686
|
}
|
|
6687
6687
|
}
|
|
6688
6688
|
visited.delete(current);
|
|
@@ -6721,9 +6721,9 @@ var upgradeSchema = (schema2) => {
|
|
|
6721
6721
|
}
|
|
6722
6722
|
return sub;
|
|
6723
6723
|
});
|
|
6724
|
-
schema2 = traverse(schema2, (sub,
|
|
6724
|
+
schema2 = traverse(schema2, (sub, path73) => {
|
|
6725
6725
|
if (sub.example !== void 0) {
|
|
6726
|
-
if (isSchemaPath(
|
|
6726
|
+
if (isSchemaPath(path73 ?? [])) {
|
|
6727
6727
|
sub.examples = [sub.example];
|
|
6728
6728
|
} else {
|
|
6729
6729
|
sub.examples = {
|
|
@@ -6736,11 +6736,11 @@ var upgradeSchema = (schema2) => {
|
|
|
6736
6736
|
}
|
|
6737
6737
|
return sub;
|
|
6738
6738
|
});
|
|
6739
|
-
schema2 = traverse(schema2, (schema3,
|
|
6739
|
+
schema2 = traverse(schema2, (schema3, path73) => {
|
|
6740
6740
|
if (schema3.type === "object" && schema3.properties !== void 0) {
|
|
6741
|
-
const parentPath =
|
|
6741
|
+
const parentPath = path73?.slice(0, -1);
|
|
6742
6742
|
const isMultipart = parentPath?.some((segment, index) => {
|
|
6743
|
-
return segment === "content" &&
|
|
6743
|
+
return segment === "content" && path73?.[index + 1] === "multipart/form-data";
|
|
6744
6744
|
});
|
|
6745
6745
|
if (isMultipart) {
|
|
6746
6746
|
const entries = Object.entries(schema3.properties);
|
|
@@ -6754,8 +6754,8 @@ var upgradeSchema = (schema2) => {
|
|
|
6754
6754
|
}
|
|
6755
6755
|
return schema3;
|
|
6756
6756
|
});
|
|
6757
|
-
schema2 = traverse(schema2, (schema3,
|
|
6758
|
-
if (
|
|
6757
|
+
schema2 = traverse(schema2, (schema3, path73) => {
|
|
6758
|
+
if (path73?.includes("content") && path73.includes("application/octet-stream")) {
|
|
6759
6759
|
return {};
|
|
6760
6760
|
}
|
|
6761
6761
|
if (schema3.type === "string" && schema3.format === "binary") {
|
|
@@ -6781,11 +6781,11 @@ var upgradeSchema = (schema2) => {
|
|
|
6781
6781
|
}
|
|
6782
6782
|
return sub;
|
|
6783
6783
|
});
|
|
6784
|
-
schema2 = traverse(schema2, (schema3,
|
|
6784
|
+
schema2 = traverse(schema2, (schema3, path73) => {
|
|
6785
6785
|
if (schema3.type === "string" && schema3.format === "byte") {
|
|
6786
|
-
const parentPath =
|
|
6786
|
+
const parentPath = path73?.slice(0, -1);
|
|
6787
6787
|
const contentMediaType = parentPath?.find(
|
|
6788
|
-
(_, index) =>
|
|
6788
|
+
(_, index) => path73?.[index - 1] === "content"
|
|
6789
6789
|
);
|
|
6790
6790
|
return {
|
|
6791
6791
|
type: "string",
|
|
@@ -6797,7 +6797,7 @@ var upgradeSchema = (schema2) => {
|
|
|
6797
6797
|
});
|
|
6798
6798
|
return schema2;
|
|
6799
6799
|
};
|
|
6800
|
-
function isSchemaPath(
|
|
6800
|
+
function isSchemaPath(path73) {
|
|
6801
6801
|
const schemaLocations = [
|
|
6802
6802
|
["components", "schemas"],
|
|
6803
6803
|
"properties",
|
|
@@ -6810,10 +6810,10 @@ function isSchemaPath(path72) {
|
|
|
6810
6810
|
];
|
|
6811
6811
|
return schemaLocations.some((location) => {
|
|
6812
6812
|
if (Array.isArray(location)) {
|
|
6813
|
-
return location.every((segment, index) =>
|
|
6813
|
+
return location.every((segment, index) => path73[index] === segment);
|
|
6814
6814
|
}
|
|
6815
|
-
return
|
|
6816
|
-
}) ||
|
|
6815
|
+
return path73.includes(location);
|
|
6816
|
+
}) || path73.includes("schema") || path73.some((segment) => segment.endsWith("Schema"));
|
|
6817
6817
|
}
|
|
6818
6818
|
|
|
6819
6819
|
// src/lib/oas/parser/index.ts
|
|
@@ -6895,13 +6895,13 @@ var OPENAPI_PROPS = /* @__PURE__ */ new Set([
|
|
|
6895
6895
|
"anyOf",
|
|
6896
6896
|
"oneOf"
|
|
6897
6897
|
]);
|
|
6898
|
-
var handleCircularRefs = (obj, currentPath = /* @__PURE__ */ new WeakSet(), refs = /* @__PURE__ */ new WeakMap(),
|
|
6898
|
+
var handleCircularRefs = (obj, currentPath = /* @__PURE__ */ new WeakSet(), refs = /* @__PURE__ */ new WeakMap(), path73 = [], currentRefPaths = /* @__PURE__ */ new Set()) => {
|
|
6899
6899
|
if (obj === null || typeof obj !== "object") return obj;
|
|
6900
6900
|
const refPath = obj.__$ref;
|
|
6901
6901
|
const isCircular = currentPath.has(obj) || typeof refPath === "string" && currentRefPaths.has(refPath);
|
|
6902
6902
|
if (isCircular) {
|
|
6903
6903
|
if (typeof refPath === "string") return SCHEMA_REF_PREFIX + refPath;
|
|
6904
|
-
const circularProp =
|
|
6904
|
+
const circularProp = path73.find((p) => !OPENAPI_PROPS.has(p)) || path73[0];
|
|
6905
6905
|
return [CIRCULAR_REF, circularProp].filter(Boolean).join(":");
|
|
6906
6906
|
}
|
|
6907
6907
|
if (refs.has(obj)) return refs.get(obj);
|
|
@@ -6911,7 +6911,7 @@ var handleCircularRefs = (obj, currentPath = /* @__PURE__ */ new WeakSet(), refs
|
|
|
6911
6911
|
value,
|
|
6912
6912
|
currentPath,
|
|
6913
6913
|
refs,
|
|
6914
|
-
[...
|
|
6914
|
+
[...path73, key],
|
|
6915
6915
|
currentRefPaths
|
|
6916
6916
|
);
|
|
6917
6917
|
const result = Array.isArray(obj) ? obj.map((item, i) => recurse(item, i.toString())) : Object.fromEntries(
|
|
@@ -6946,7 +6946,7 @@ var resolveExtensions = (obj) => Object.fromEntries(
|
|
|
6946
6946
|
var getAllTags = (schema2) => {
|
|
6947
6947
|
const rootTags = schema2.tags ?? [];
|
|
6948
6948
|
const operations = Object.values(schema2.paths ?? {}).flatMap(
|
|
6949
|
-
(
|
|
6949
|
+
(path73) => HttpMethods.map((k) => path73?.[k]).filter((op) => op != null)
|
|
6950
6950
|
);
|
|
6951
6951
|
const operationTags = new Set(operations.flatMap((op) => op.tags ?? []));
|
|
6952
6952
|
const hasUntaggedOperations = operations.some(
|
|
@@ -7001,7 +7001,7 @@ var getAllSlugs = (ops, schemaTags = []) => {
|
|
|
7001
7001
|
var getOperationSlugKey = (op) => [op.path, op.method, op.operationId, op.summary].filter(Boolean).join("-");
|
|
7002
7002
|
var getAllOperations = (paths) => {
|
|
7003
7003
|
const operations = Object.entries(paths ?? {}).flatMap(
|
|
7004
|
-
([
|
|
7004
|
+
([path73, value]) => HttpMethods.flatMap((method) => {
|
|
7005
7005
|
if (!value?.[method]) return [];
|
|
7006
7006
|
const operation = value[method];
|
|
7007
7007
|
const pathParameters = value.parameters ?? [];
|
|
@@ -7021,7 +7021,7 @@ var getAllOperations = (paths) => {
|
|
|
7021
7021
|
return {
|
|
7022
7022
|
...operation,
|
|
7023
7023
|
method,
|
|
7024
|
-
path:
|
|
7024
|
+
path: path73,
|
|
7025
7025
|
parameters,
|
|
7026
7026
|
servers,
|
|
7027
7027
|
tags: operation.tags ?? []
|
|
@@ -7379,8 +7379,8 @@ var Schema = builder.objectRef("Schema").implement({
|
|
|
7379
7379
|
}),
|
|
7380
7380
|
paths: t.field({
|
|
7381
7381
|
type: [PathItem],
|
|
7382
|
-
resolve: (root) => Object.entries(root.paths ?? {}).map(([
|
|
7383
|
-
path:
|
|
7382
|
+
resolve: (root) => Object.entries(root.paths ?? {}).map(([path73, value]) => ({
|
|
7383
|
+
path: path73,
|
|
7384
7384
|
// biome-ignore lint/style/noNonNullAssertion: value is guaranteed to be defined
|
|
7385
7385
|
methods: Object.keys(value)
|
|
7386
7386
|
}))
|
|
@@ -7545,7 +7545,7 @@ init_joinUrl();
|
|
|
7545
7545
|
|
|
7546
7546
|
// src/vite/api/schema-codegen.ts
|
|
7547
7547
|
var unescapeJsonPointer = (uri) => decodeURIComponent(uri.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
7548
|
-
var getSegmentsFromPath = (
|
|
7548
|
+
var getSegmentsFromPath = (path73) => path73.split("/").slice(1).map(unescapeJsonPointer);
|
|
7549
7549
|
var createLocalRefMap = (obj) => {
|
|
7550
7550
|
const refMap = /* @__PURE__ */ new Map();
|
|
7551
7551
|
const siblingsMap = /* @__PURE__ */ new Map();
|
|
@@ -7576,16 +7576,16 @@ var replaceMarkers = (code, mergedRefs) => code.replace(/"__refMap:(.*?)"/g, '__
|
|
|
7576
7576
|
/"__refMap\+Siblings:(.*?)"/g,
|
|
7577
7577
|
(_, key) => mergedRefs.get(key) ?? `__refMap["${key}"]`
|
|
7578
7578
|
);
|
|
7579
|
-
var lookup = (schema2,
|
|
7580
|
-
const parts = getSegmentsFromPath(
|
|
7579
|
+
var lookup = (schema2, path73, filePath) => {
|
|
7580
|
+
const parts = getSegmentsFromPath(path73);
|
|
7581
7581
|
let val = schema2;
|
|
7582
7582
|
for (const part of parts) {
|
|
7583
7583
|
while (val.$ref?.startsWith("#/")) {
|
|
7584
|
-
val = val.$ref ===
|
|
7584
|
+
val = val.$ref === path73 ? val : lookup(schema2, val.$ref, filePath);
|
|
7585
7585
|
}
|
|
7586
7586
|
if (val[part] === void 0) {
|
|
7587
7587
|
throw new Error(
|
|
7588
|
-
`Error in ${filePath ?? "code generation"}: Could not find path segment ${part} in path: ${
|
|
7588
|
+
`Error in ${filePath ?? "code generation"}: Could not find path segment ${part} in path: ${path73}`
|
|
7589
7589
|
);
|
|
7590
7590
|
}
|
|
7591
7591
|
val = val[part];
|
|
@@ -7830,8 +7830,8 @@ var SchemaManager = class {
|
|
|
7830
7830
|
}
|
|
7831
7831
|
}
|
|
7832
7832
|
};
|
|
7833
|
-
getLatestSchema = (
|
|
7834
|
-
getSchemasForPath = (
|
|
7833
|
+
getLatestSchema = (path73) => this.processedSchemas[path73]?.at(0);
|
|
7834
|
+
getSchemasForPath = (path73) => this.processedSchemas[path73];
|
|
7835
7835
|
getSchemaImports = () => Object.values(this.processedSchemas).flat().filter((s) => s.importKey);
|
|
7836
7836
|
getUrlToFilePathMap = () => {
|
|
7837
7837
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -10269,14 +10269,14 @@ async function generateSitemap({
|
|
|
10269
10269
|
|
|
10270
10270
|
// src/vite/prerender/utils.ts
|
|
10271
10271
|
init_joinUrl();
|
|
10272
|
-
var resolveRoutePath = (
|
|
10273
|
-
const segments =
|
|
10272
|
+
var resolveRoutePath = (path73) => {
|
|
10273
|
+
const segments = path73.split("/");
|
|
10274
10274
|
if (segments.some((s) => s.startsWith(":") && !s.endsWith("?"))) {
|
|
10275
10275
|
return void 0;
|
|
10276
10276
|
}
|
|
10277
10277
|
return segments.filter((s) => !s.startsWith(":")).join("/") || void 0;
|
|
10278
10278
|
};
|
|
10279
|
-
var isSkipped = (
|
|
10279
|
+
var isSkipped = (path73) => path73.includes("*") || /^\d+$/.test(path73);
|
|
10280
10280
|
var resolveRoutes = (routes, parentPath = "") => routes.flatMap((route) => {
|
|
10281
10281
|
if (route.path && isSkipped(route.path)) return [];
|
|
10282
10282
|
const routePath = route.path ? resolveRoutePath(route.path) : void 0;
|
|
@@ -10908,8 +10908,8 @@ var PlatformApiClient = class {
|
|
|
10908
10908
|
constructor(config2) {
|
|
10909
10909
|
this.config = config2;
|
|
10910
10910
|
}
|
|
10911
|
-
url(
|
|
10912
|
-
return `${this.config.apiUrl}${
|
|
10911
|
+
url(path73) {
|
|
10912
|
+
return `${this.config.apiUrl}${path73.startsWith("/") ? path73 : `/${path73}`}`;
|
|
10913
10913
|
}
|
|
10914
10914
|
headers(extra) {
|
|
10915
10915
|
return {
|
|
@@ -10919,9 +10919,9 @@ var PlatformApiClient = class {
|
|
|
10919
10919
|
...extra
|
|
10920
10920
|
};
|
|
10921
10921
|
}
|
|
10922
|
-
async request(method,
|
|
10922
|
+
async request(method, path73, init4) {
|
|
10923
10923
|
const response = await fetchWithRetry(
|
|
10924
|
-
this.url(
|
|
10924
|
+
this.url(path73),
|
|
10925
10925
|
{
|
|
10926
10926
|
method,
|
|
10927
10927
|
...init4,
|
|
@@ -11133,15 +11133,15 @@ var PlatformApiClient = class {
|
|
|
11133
11133
|
}
|
|
11134
11134
|
);
|
|
11135
11135
|
}
|
|
11136
|
-
deleteDevPortalPage(projectId,
|
|
11137
|
-
const query = new URLSearchParams({ path:
|
|
11136
|
+
deleteDevPortalPage(projectId, path73) {
|
|
11137
|
+
const query = new URLSearchParams({ path: path73 });
|
|
11138
11138
|
return this.request(
|
|
11139
11139
|
"DELETE",
|
|
11140
11140
|
`/api/v1/projects/${projectId}/dev-portal/pages/files?${query.toString()}`
|
|
11141
11141
|
);
|
|
11142
11142
|
}
|
|
11143
|
-
renameDevPortalPage(projectId,
|
|
11144
|
-
const query = new URLSearchParams({ path:
|
|
11143
|
+
renameDevPortalPage(projectId, path73, newName) {
|
|
11144
|
+
const query = new URLSearchParams({ path: path73 });
|
|
11145
11145
|
return this.request(
|
|
11146
11146
|
"POST",
|
|
11147
11147
|
`/api/v1/projects/${projectId}/dev-portal/pages/files/rename?${query.toString()}`,
|
|
@@ -16205,7 +16205,7 @@ async function ensureTsconfigJson(dir) {
|
|
|
16205
16205
|
return true;
|
|
16206
16206
|
}
|
|
16207
16207
|
}
|
|
16208
|
-
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.",\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: "$299",\n unit: "per month",\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';
|
|
16208
|
+
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 {/* Static sample list. To show your real API operations from the dev portal\n backend instead, see https://docs.apitogo.com/docs/customization/landing-page#live-apis */}\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.",\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: "$299",\n unit: "per month",\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';
|
|
16209
16209
|
var ACCOUNT_PLANS_PAGE_SOURCE = `export { AccountPlansPage as default } from "@lukoweb/apitogo-plugin-dev-portal-billing";
|
|
16210
16210
|
`;
|
|
16211
16211
|
var DASHBOARD_PAGE_SOURCE = `export { OverviewDashboardPage as default } from "@lukoweb/apitogo-plugin-dev-portal-billing";
|
|
@@ -17129,8 +17129,8 @@ function normalizeActionKey(actionKey) {
|
|
|
17129
17129
|
const trimmed = actionKey.trim();
|
|
17130
17130
|
if (trimmed.includes(":")) {
|
|
17131
17131
|
const [method2, ...pathParts] = trimmed.split(":");
|
|
17132
|
-
const
|
|
17133
|
-
return `${method2?.toLowerCase()}:${
|
|
17132
|
+
const path74 = pathParts.join(":").trim();
|
|
17133
|
+
return `${method2?.toLowerCase()}:${path74.startsWith("/") ? path74 : `/${path74}`}`;
|
|
17134
17134
|
}
|
|
17135
17135
|
const spaceIndex = trimmed.indexOf(" ");
|
|
17136
17136
|
if (spaceIndex <= 0) {
|
|
@@ -17139,8 +17139,8 @@ function normalizeActionKey(actionKey) {
|
|
|
17139
17139
|
);
|
|
17140
17140
|
}
|
|
17141
17141
|
const method = trimmed.slice(0, spaceIndex).trim().toLowerCase();
|
|
17142
|
-
const
|
|
17143
|
-
return `${method}:${
|
|
17142
|
+
const path73 = trimmed.slice(spaceIndex + 1).trim();
|
|
17143
|
+
return `${method}:${path73.startsWith("/") ? path73 : `/${path73}`}`;
|
|
17144
17144
|
}
|
|
17145
17145
|
async function applyGatewayPublicRoutes(client, projectId, routes, options) {
|
|
17146
17146
|
const applied = [];
|
|
@@ -17758,8 +17758,8 @@ function normalizeActionKeyFormat(actionKey) {
|
|
|
17758
17758
|
const trimmed = actionKey.trim();
|
|
17759
17759
|
if (trimmed.includes(":")) {
|
|
17760
17760
|
const [method2, ...pathParts] = trimmed.split(":");
|
|
17761
|
-
const
|
|
17762
|
-
return `${method2.toLowerCase()}:${
|
|
17761
|
+
const path74 = pathParts.join(":").trim();
|
|
17762
|
+
return `${method2.toLowerCase()}:${path74.startsWith("/") ? path74 : `/${path74}`}`;
|
|
17763
17763
|
}
|
|
17764
17764
|
const spaceIndex = trimmed.indexOf(" ");
|
|
17765
17765
|
if (spaceIndex <= 0) {
|
|
@@ -17768,8 +17768,8 @@ function normalizeActionKeyFormat(actionKey) {
|
|
|
17768
17768
|
);
|
|
17769
17769
|
}
|
|
17770
17770
|
const method = trimmed.slice(0, spaceIndex).trim().toLowerCase();
|
|
17771
|
-
const
|
|
17772
|
-
return `${method}:${
|
|
17771
|
+
const path73 = trimmed.slice(spaceIndex + 1).trim();
|
|
17772
|
+
return `${method}:${path73.startsWith("/") ? path73 : `/${path73}`}`;
|
|
17773
17773
|
}
|
|
17774
17774
|
function normalizePublicRoute(route) {
|
|
17775
17775
|
const trimmed = route.trim();
|
|
@@ -18273,17 +18273,14 @@ function isWarmPoolInfrastructureTerminal(project) {
|
|
|
18273
18273
|
return status2 === "ready" || status2 === "failed" || status2 === "deleting";
|
|
18274
18274
|
}
|
|
18275
18275
|
function shouldSkipColdModuleInfrastructureWait(project, infraStatus) {
|
|
18276
|
-
if (isWarmPoolInfrastructureReady(project)) {
|
|
18277
|
-
return true;
|
|
18278
|
-
}
|
|
18279
18276
|
const resourceType = infraStatus?.resourceType;
|
|
18280
|
-
if (resourceType === "WarmPoolBinding") {
|
|
18281
|
-
return true;
|
|
18282
|
-
}
|
|
18283
18277
|
if (resourceType && COLD_MODULE_INFRA_RESOURCE_TYPES.has(resourceType)) {
|
|
18284
18278
|
return false;
|
|
18285
18279
|
}
|
|
18286
|
-
|
|
18280
|
+
if (resourceType === "WarmPoolBinding") {
|
|
18281
|
+
return true;
|
|
18282
|
+
}
|
|
18283
|
+
return isWarmPoolInfrastructureReady(project);
|
|
18287
18284
|
}
|
|
18288
18285
|
function resolvePortalBackendFqdn(project) {
|
|
18289
18286
|
const portalApiUrl = project.portalApiUrl?.trim();
|
|
@@ -22812,8 +22809,8 @@ import readline from "node:readline/promises";
|
|
|
22812
22809
|
import open2 from "open";
|
|
22813
22810
|
|
|
22814
22811
|
// src/cli/platform/device-auth.ts
|
|
22815
|
-
function url(apiUrl,
|
|
22816
|
-
return `${apiUrl}${
|
|
22812
|
+
function url(apiUrl, path73) {
|
|
22813
|
+
return `${apiUrl}${path73.startsWith("/") ? path73 : `/${path73}`}`;
|
|
22817
22814
|
}
|
|
22818
22815
|
async function requestDeviceCode(apiUrl) {
|
|
22819
22816
|
const response = await fetchWithRetry(
|
|
@@ -23730,8 +23727,79 @@ function createApitogoCli(argv) {
|
|
|
23730
23727
|
}).middleware(warnPackageVersionMismatch).command(command(init_default)).command(command(config_default)).command(command(run_default)).command(command(build_default)).command(command(check_default)).command(command(project_create_default)).command(command(deploy_default)).command(command(status_default)).command(command(teardown_default)).command(command(login_default)).command(command(logout_default)).demandCommand().strictCommands().version(packageJson?.version).fail(false).help();
|
|
23731
23728
|
}
|
|
23732
23729
|
|
|
23730
|
+
// src/cli/local-handoff.ts
|
|
23731
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
23732
|
+
import { existsSync as existsSync6, realpathSync } from "node:fs";
|
|
23733
|
+
import path72 from "node:path";
|
|
23734
|
+
var LOCAL_HANDOFF_ENV = "APITOGO_LOCAL_CLI";
|
|
23735
|
+
var PACKAGE_NAME = "@lukoweb/apitogo";
|
|
23736
|
+
var resolveProjectDir = (argv, cwd) => {
|
|
23737
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23738
|
+
const arg = argv[i];
|
|
23739
|
+
const next = argv[i + 1];
|
|
23740
|
+
if (arg === "--dir" && next) return path72.resolve(cwd, next);
|
|
23741
|
+
if (arg?.startsWith("--dir=")) {
|
|
23742
|
+
return path72.resolve(cwd, arg.slice("--dir=".length));
|
|
23743
|
+
}
|
|
23744
|
+
}
|
|
23745
|
+
return cwd;
|
|
23746
|
+
};
|
|
23747
|
+
var findLocalCli = (projectDir, runningRoot) => {
|
|
23748
|
+
let dir = path72.resolve(projectDir);
|
|
23749
|
+
let localPackageRoot;
|
|
23750
|
+
while (!localPackageRoot) {
|
|
23751
|
+
const candidate = path72.join(dir, "node_modules", PACKAGE_NAME);
|
|
23752
|
+
if (existsSync6(path72.join(candidate, "package.json"))) {
|
|
23753
|
+
localPackageRoot = candidate;
|
|
23754
|
+
} else if (dir === path72.dirname(dir)) {
|
|
23755
|
+
return void 0;
|
|
23756
|
+
} else {
|
|
23757
|
+
dir = path72.dirname(dir);
|
|
23758
|
+
}
|
|
23759
|
+
}
|
|
23760
|
+
const localRoot = realpathSync(localPackageRoot);
|
|
23761
|
+
if (localRoot === realpathSync(runningRoot)) return void 0;
|
|
23762
|
+
return path72.join(localRoot, "cli.js");
|
|
23763
|
+
};
|
|
23764
|
+
var handoffToLocalCli = async (argv, runningRoot) => {
|
|
23765
|
+
if (process.env[LOCAL_HANDOFF_ENV] || process.env.ZUDOKU_ENV === "internal") {
|
|
23766
|
+
return void 0;
|
|
23767
|
+
}
|
|
23768
|
+
const localCli = findLocalCli(
|
|
23769
|
+
resolveProjectDir(argv, process.cwd()),
|
|
23770
|
+
runningRoot
|
|
23771
|
+
);
|
|
23772
|
+
if (!localCli) return void 0;
|
|
23773
|
+
printDiagnosticsToConsole(
|
|
23774
|
+
`Using the project's local ${PACKAGE_NAME} (${path72.dirname(localCli)})`
|
|
23775
|
+
);
|
|
23776
|
+
const child = spawn4(process.execPath, [localCli, ...argv], {
|
|
23777
|
+
stdio: "inherit",
|
|
23778
|
+
env: { ...process.env, [LOCAL_HANDOFF_ENV]: "1" }
|
|
23779
|
+
});
|
|
23780
|
+
process.on("SIGINT", () => {
|
|
23781
|
+
});
|
|
23782
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
23783
|
+
return new Promise((resolve) => {
|
|
23784
|
+
child.on("error", (err) => {
|
|
23785
|
+
printDiagnosticsToConsole(
|
|
23786
|
+
`Failed to start local ${PACKAGE_NAME}: ${err.message}`
|
|
23787
|
+
);
|
|
23788
|
+
resolve(1);
|
|
23789
|
+
});
|
|
23790
|
+
child.on("exit", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
23791
|
+
});
|
|
23792
|
+
};
|
|
23793
|
+
|
|
23733
23794
|
// src/cli/cli.ts
|
|
23734
23795
|
process.env.ZUDOKU_ENV = process.env.ZUDOKU_INTERNAL_DEV ? "internal" : "module";
|
|
23796
|
+
var handoffExitCode = await handoffToLocalCli(
|
|
23797
|
+
hideBin(process.argv),
|
|
23798
|
+
getZudokuRootDir()
|
|
23799
|
+
);
|
|
23800
|
+
if (handoffExitCode !== void 0) {
|
|
23801
|
+
process.exit(handoffExitCode);
|
|
23802
|
+
}
|
|
23735
23803
|
var packageJson2 = getZudokuPackageJson();
|
|
23736
23804
|
if (SENTRY_DSN) {
|
|
23737
23805
|
Sentry2.init({
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_icon: house
|
|
3
|
+
title: Landing Page
|
|
4
|
+
description:
|
|
5
|
+
Customize your dev portal home page and show your real API operations in the API showcase, loaded
|
|
6
|
+
live from your dev portal backend.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
A scaffolded dev portal home page lives in `landing/LandingPage.tsx`. It composes section components
|
|
10
|
+
from `@lukoweb/apitogo-module-landing` inside a `LandingShell`:
|
|
11
|
+
|
|
12
|
+
| Component | Purpose |
|
|
13
|
+
| --------------------- | --------------------------------------------- |
|
|
14
|
+
| `HeroSection` | Headline, description, CTAs and code examples |
|
|
15
|
+
| `TrustSection` | Logos or social proof |
|
|
16
|
+
| `FeaturesSection` | Feature grid |
|
|
17
|
+
| `StepsSection` | Numbered "how it works" steps |
|
|
18
|
+
| `ApiShowcaseSection` | Grid or method + path list of your API |
|
|
19
|
+
| `PlansPreviewSection` | Short plan comparison linking to `/pricing` |
|
|
20
|
+
| `PricingCtaSection` | Closing call to action |
|
|
21
|
+
|
|
22
|
+
Edit the file directly to add, remove or reorder sections. The site footer is configured separately
|
|
23
|
+
in `apitogo.config.json` under `modules.landing.content.footer` (see
|
|
24
|
+
[Footer](/docs/configuration/footer)).
|
|
25
|
+
|
|
26
|
+
## API showcase
|
|
27
|
+
|
|
28
|
+
`ApiShowcaseSection` renders the items you pass it. The scaffold ships with a **static sample
|
|
29
|
+
list**:
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<ApiShowcaseSection
|
|
33
|
+
apiShowcase={{
|
|
34
|
+
title: "Sample endpoints",
|
|
35
|
+
layout: "list", // or "grid"
|
|
36
|
+
browseAllHref: "/api",
|
|
37
|
+
items: [
|
|
38
|
+
{ name: "List users", path: "/v1/users", method: "GET", summary: "List paginated users" },
|
|
39
|
+
],
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Each item accepts `name`, `path`, and optionally `method`, `summary`, `icon` and `latency`.
|
|
45
|
+
|
|
46
|
+
## Live APIs
|
|
47
|
+
|
|
48
|
+
Instead of maintaining the list by hand, you can load your API's real operations. Your dev portal
|
|
49
|
+
backend syncs the **gateway actions** of your project (every operation discovered from the OpenAPI
|
|
50
|
+
spec you deployed) and serves them publicly at `GET /api/v1/actions`. The `useGatewayActions()` hook
|
|
51
|
+
fetches that list; you decide what to show and pass the result to `ApiShowcaseSection`.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import {
|
|
55
|
+
ApiShowcaseSection,
|
|
56
|
+
LandingShell,
|
|
57
|
+
toApiShowcaseItems,
|
|
58
|
+
useGatewayActions,
|
|
59
|
+
} from "@lukoweb/apitogo-module-landing";
|
|
60
|
+
|
|
61
|
+
const staticItems = [
|
|
62
|
+
{ name: "List users", path: "/v1/users", method: "GET", summary: "List paginated users" },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Optional friendly labels, keyed by action key (`method:path`, lower-case method).
|
|
66
|
+
const labels: Record<string, string> = {
|
|
67
|
+
"get:/v1/users": "List paginated users",
|
|
68
|
+
"post:/v1/orders": "Create an order",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default function LandingPage() {
|
|
72
|
+
const { data } = useGatewayActions({ inPlan: true });
|
|
73
|
+
|
|
74
|
+
const liveItems = toApiShowcaseItems(
|
|
75
|
+
(data?.actions ?? []).filter((action) => action.path.startsWith("/v1/")).slice(0, 6),
|
|
76
|
+
labels,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<LandingShell title="My API">
|
|
81
|
+
<ApiShowcaseSection
|
|
82
|
+
apiShowcase={{
|
|
83
|
+
title: "Our APIs",
|
|
84
|
+
layout: "list",
|
|
85
|
+
browseAllHref: "/api",
|
|
86
|
+
items: liveItems.length > 0 ? liveItems : staticItems,
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
</LandingShell>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Filters
|
|
95
|
+
|
|
96
|
+
`useGatewayActions()` accepts server-side filters. All filters are combined:
|
|
97
|
+
|
|
98
|
+
| Option | Type | Description |
|
|
99
|
+
| ------------ | --------- | ------------------------------------------------------ |
|
|
100
|
+
| `inPlan` | `boolean` | Only actions included in at least one active plan |
|
|
101
|
+
| `plan` | `string` | Only actions included in the plan with this SKU |
|
|
102
|
+
| `isPublic` | `boolean` | Only actions callable without an API key (or only not) |
|
|
103
|
+
| `method` | `string` | HTTP method, e.g. `GET` |
|
|
104
|
+
| `pathPrefix` | `string` | Path prefix, e.g. `/v1/orders` |
|
|
105
|
+
| `limit` | `number` | Maximum number of actions (server cap: 500) |
|
|
106
|
+
| `enabled` | `boolean` | Set to `false` to skip the request |
|
|
107
|
+
|
|
108
|
+
Anything else — sorting, renaming, grouping, picking a hand-curated subset — is up to you in plain
|
|
109
|
+
TypeScript before you build the items.
|
|
110
|
+
|
|
111
|
+
Each action has this shape:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
type GatewayAction = {
|
|
115
|
+
actionKey: string; // e.g. "get:/orders/{id}"
|
|
116
|
+
method: string; // e.g. "GET"
|
|
117
|
+
path: string; // e.g. "/orders/{id}"
|
|
118
|
+
isPublic: boolean; // callable without an API key
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Actions only carry the method and path. Use the `summaries` argument of `toApiShowcaseItems()` (or
|
|
123
|
+
your own mapping) to add human-friendly names.
|
|
124
|
+
|
|
125
|
+
### Always keep a fallback
|
|
126
|
+
|
|
127
|
+
The hook runs in the browser only, so the prerendered HTML never contains a stale list. `data` is
|
|
128
|
+
`undefined`:
|
|
129
|
+
|
|
130
|
+
- while the request is loading,
|
|
131
|
+
- if the request fails,
|
|
132
|
+
- in preview and demo mode, or when no dev portal backend URL is configured (for example in
|
|
133
|
+
`npm run dev` before your portal is provisioned).
|
|
134
|
+
|
|
135
|
+
`ApiShowcaseSection` renders nothing for an empty list, so pass a static fallback as shown above.
|
|
136
|
+
|
|
137
|
+
### What becomes public
|
|
138
|
+
|
|
139
|
+
`GET /api/v1/actions` is anonymous: **every operation in the OpenAPI spec deployed to your gateway
|
|
140
|
+
is listable**, including ones you may not document in the portal. Use `inPlan: true` to limit the
|
|
141
|
+
list to operations your plans actually sell, and remove internal operations from the spec you deploy
|
|
142
|
+
to the gateway if they must not be discoverable.
|
|
143
|
+
|
|
144
|
+
### Freshness and limits
|
|
145
|
+
|
|
146
|
+
- The backend syncs actions at startup and on the plan sync interval (15 minutes by default). A new
|
|
147
|
+
release shows up after the next sync.
|
|
148
|
+
- Responses are cacheable for 5 minutes (`Cache-Control: public, max-age=300`).
|
|
149
|
+
- The endpoint is rate limited per client IP (60 requests per minute).
|
|
150
|
+
|
|
151
|
+
### Troubleshooting
|
|
152
|
+
|
|
153
|
+
- **The list is always the fallback:** check that `authentication.apiBaseUrl` (or
|
|
154
|
+
`VITE_APITOGO_AUTHENTICATION_API_BASE_URL`) points at your dev portal backend and is not the
|
|
155
|
+
placeholder URL.
|
|
156
|
+
- **CORS errors:** the backend only allows the portal's configured frontend URL. Serving the site
|
|
157
|
+
from another domain requires adding it to the backend's allowed origins.
|
|
158
|
+
- **Empty list on a new portal:** the portal is not activated yet, or the gateway has no release.
|
|
159
|
+
Deploy your backend, then wait for the next sync.
|