@lukoweb/apitogo 0.2.50 → 0.2.52

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.
@@ -18,7 +18,13 @@ export type ReferenceOptions = {
18
18
  };
19
19
 
20
20
  /** Header of the API key when the gateway does not say otherwise (same as the platform's OpenAPI export). */
21
- export const DEFAULT_KEY_HEADER = "Ocp-Apim-Subscription-Key";
21
+ export const DEFAULT_KEY_HEADER = "X-API-Key";
22
+
23
+ /**
24
+ * Operation extension naming the header a protected operation's key goes in. The API page's code samples read it
25
+ * (`createHttpSnippet`), so they show the header with a placeholder key.
26
+ */
27
+ export const KEY_HEADER_EXTENSION = "x-apitogo-key-header";
22
28
 
23
29
  const METHODS = [
24
30
  "get",
@@ -175,10 +181,17 @@ export function buildReferenceSpec(
175
181
  else delete doc.components;
176
182
  // One requirement with both schemes means both are needed together.
177
183
  doc.security = Object.keys(requirement).length > 0 ? [requirement] : [];
178
- for (const [, item, method] of operationsOf(paths)) {
184
+ for (const [path, item, method] of operationsOf(paths)) {
179
185
  const operation = item[method] as Json;
180
186
  if (Array.isArray(operation.security) && operation.security.length > 0)
181
187
  delete operation.security;
188
+ // The page's code samples add this header, so a copied sample works once the key is filled in (API-120).
189
+ if (
190
+ options.apiKeyRequired &&
191
+ !options.publicOperations.has(actionKey(method, path))
192
+ )
193
+ operation[KEY_HEADER_EXTENSION] = options.apiKeyHeader;
194
+ else delete operation[KEY_HEADER_EXTENSION];
182
195
  }
183
196
 
184
197
  return JSON.parse(hideOrigin(JSON.stringify(doc), options)) as Json;
@@ -59,7 +59,7 @@ title: Welcome
59
59
 
60
60
  # ${name}
61
61
 
62
- This page was created by \`apitogo init\`. Edit it, and add more \`.md\` or \`.mdx\` files next to it: each one becomes a page of your portal.
62
+ Welcome to the ${name} developer portal. Here you will find guides, the API reference, and everything you need to get an API key and make your first call.
63
63
  `;
64
64
  }
65
65
 
@@ -9,15 +9,22 @@ export function starterLandingPage(options: {
9
9
  }): string {
10
10
  const name = JSON.stringify(options.name);
11
11
  const plansImport = options.withPlans
12
- ? 'import { usePlansPreview } from "@lukoweb/apitogo-plugin-dev-portal-billing";\n'
12
+ ? 'import {\n useGatewayCall,\n usePlansPreview,\n} from "@lukoweb/apitogo-plugin-dev-portal-billing";\n'
13
13
  : "";
14
14
  const plansHook = options.withPlans
15
- ? " // The plans of the project's catalog: the same ones the pricing page shows.\n const plans = usePlansPreview();\n\n"
15
+ ? " // The plans of the project's catalog: the same ones the pricing page shows.\n const plans = usePlansPreview();\n // The real gateway address and key header, so the sample works when it is pasted with a key.\n const gateway = useGatewayCall();\n\n"
16
16
  : "";
17
+ // Without the plans service the page cannot learn the gateway, so the sample names the platform's default header.
18
+ const sampleUrl = options.withPlans
19
+ ? '${gateway.baseUrl ?? "https://YOUR-GATEWAY"}'
20
+ : "https://YOUR-GATEWAY";
21
+ const sampleHeader = options.withPlans ? "${gateway.keyHeader}" : "X-API-Key";
17
22
  const plansSection = options.withPlans
18
23
  ? " <PlansPreviewSection plans={plans} />\n"
19
24
  : "";
20
- return `import {
25
+ return `// Your portal's home page. Edit this file (apitogo/portal/landing/LandingPage.tsx) to make it yours: everything
26
+ // on it is public, so keep notes for yourself in comments like this one.
27
+ import {
21
28
  ApiShowcaseSection,
22
29
  FeaturesSection,
23
30
  HeroSection,
@@ -47,8 +54,8 @@ ${plansHook} return (
47
54
  {
48
55
  filename: "Call the API",
49
56
  language: "bash",
50
- code: \`$ curl https://your-api.example.com/health \\\\
51
- -H "x-api-key: YOUR_API_KEY"
57
+ code: \`$ curl ${sampleUrl}/health \\\\
58
+ -H "${sampleHeader}: YOUR_API_KEY"
52
59
  # { "status": "ok" }\`,
53
60
  },
54
61
  ]}
@@ -58,7 +65,7 @@ ${plansHook} return (
58
65
  label: "What you get",
59
66
  title: "A developer portal that is ready on day one",
60
67
  description:
61
- "Edit this page in apitogo/portal/landing/LandingPage.tsx to make it yours.",
68
+ "Docs, a live API reference, self-service API keys and clear plans, all in one place.",
62
69
  }}
63
70
  features={[
64
71
  {
@@ -41,6 +41,18 @@ export function contentGlob(configDir: string, glob: string): string {
41
41
  return `${negated ? "!" : ""}/${CONTENT_LINK}/${relative}`;
42
42
  }
43
43
 
44
+ /**
45
+ * A project file as the generated site root sees it: `content/<path in the project>`, which the build resolves against
46
+ * the site root. The generated config is imported by the portal's code, so an absolute path would end up in the public
47
+ * bundle (API-127). A file outside the project has no such path and keeps its absolute one.
48
+ */
49
+ export function contentFile(configDir: string, file: string): string {
50
+ const relative = path.relative(configDir, file);
51
+ if (relative.startsWith("..") || path.isAbsolute(relative))
52
+ return file.split(path.sep).join("/");
53
+ return `${CONTENT_LINK}/${relative.split(path.sep).join("/")}`;
54
+ }
55
+
44
56
  /** Capabilities of the plans service that put something on the console. Any one of them registers the plans plugin. */
45
57
  const PLANS_CONSOLE_CAPABILITIES = [
46
58
  "catalog",
@@ -77,6 +89,10 @@ export type PortalPlan = {
77
89
  billing?: string;
78
90
  /** Where the account pages link to: only places this portal has (hosted docs, an external docs site, the API page). */
79
91
  links?: { docs?: string; api?: string };
92
+ /** The header API users send their key in, from the gateway entry of services.json (API-120). */
93
+ keyHeader?: string;
94
+ /** The gateway's address, for code samples on the portal's pages. */
95
+ gatewayBaseUrl?: string;
80
96
  };
81
97
  /** Account pages. */
82
98
  userPanel: boolean;
@@ -259,6 +275,10 @@ export function planPortal(
259
275
  apiBaseUrl: plans.apiBaseUrl,
260
276
  keysApiBaseUrl: plans.keysApiBaseUrl,
261
277
  billing: plans.billing,
278
+ ...(gateway?.keyHeaderName ? { keyHeader: gateway.keyHeaderName } : {}),
279
+ ...(gateway?.baseUrl && serviceReady(gateway)
280
+ ? { gatewayBaseUrl: gateway.baseUrl }
281
+ : {}),
262
282
  links: {
263
283
  // Hosted docs are linked once their pages are known (see `buildPortalWorkspace`): there is no page to
264
284
  // name before that, and a guessed one was a 404 on every portal whose first page is not called `introduction`.
@@ -326,14 +346,14 @@ export function buildSiteConfig(
326
346
  },
327
347
  modules: {
328
348
  docs: { enabled: plan.docs },
329
- // A custom home page (`portal.landing`) is served at `/`; the build imports the component from its absolute path.
349
+ // A custom home page (`portal.landing`) is served at `/`; the build imports the component through the content link.
330
350
  ...(portal?.landing
331
351
  ? {
332
352
  landing: {
333
353
  enabled: true,
334
354
  path: "/",
335
355
  layout: "landing",
336
- page: portal.landing.split(path.sep).join("/"),
356
+ page: contentFile(resolved.configDir, portal.landing),
337
357
  },
338
358
  }
339
359
  : {}),
@@ -1,135 +1,147 @@
1
- import { HTTPSnippet } from "@zudoku/httpsnippet";
2
- import type { OperationsFragmentFragment } from "../graphql/graphql.js";
3
-
4
- const toFormDataParams = (text?: string) => {
5
- const stringify = (v: unknown) =>
6
- typeof v === "string" ? v : JSON.stringify(v);
7
-
8
- try {
9
- const obj = text && JSON.parse(text);
10
- if (typeof obj !== "object" || !obj) return [];
11
-
12
- return Object.entries(obj).flatMap(([name, value]) => {
13
- const values = Array.isArray(value) ? value : [value];
14
- return values.map((v) => ({ name, value: stringify(v) }));
15
- });
16
- } catch {
17
- return [];
18
- }
19
- };
20
-
21
- export const createHttpSnippet = ({
22
- operation,
23
- selectedServer,
24
- exampleBody,
25
- }: {
26
- operation: OperationsFragmentFragment;
27
- selectedServer: string;
28
- exampleBody: {
29
- mimeType: string;
30
- text?: string;
31
- };
32
- }) => {
33
- const isMultipart =
34
- exampleBody.mimeType === "multipart/form-data" ||
35
- exampleBody.mimeType === "application/x-www-form-urlencoded";
36
-
37
- const postData = isMultipart
38
- ? {
39
- mimeType: exampleBody.mimeType,
40
- params: toFormDataParams(exampleBody.text),
41
- }
42
- : exampleBody;
43
-
44
- return new HTTPSnippet({
45
- method: operation.method.toUpperCase(),
46
- url:
47
- selectedServer + operation.path.replaceAll("{", ":").replaceAll("}", ""),
48
- postData,
49
- headers: [
50
- ...(exampleBody.text
51
- ? [{ name: "Content-Type", value: exampleBody.mimeType }]
52
- : []),
53
- ...(operation.parameters
54
- ?.filter((p) => p.in === "header" && p.required === true)
55
- .map((p) => ({
56
- name: p.name,
57
- value:
58
- p.schema?.default ??
59
- p.examples?.find((x) => x.value)?.value ??
60
- (p.schema?.type === "string"
61
- ? "<string>"
62
- : p.schema?.type === "number" || p.schema?.type === "integer"
63
- ? "<number>"
64
- : p.schema?.type === "boolean"
65
- ? "<bool>"
66
- : "<value>"),
67
- })) ?? []),
68
- ],
69
- queryString:
70
- operation.parameters
71
- ?.filter((p) => p.in === "query" && p.required === true)
72
- .map((p) => ({
73
- name: p.name,
74
- value:
75
- p.schema?.default ??
76
- p.examples?.find((x) => x.value)?.value ??
77
- (p.schema?.type === "string"
78
- ? "<string>"
79
- : p.schema?.type === "number" || p.schema?.type === "integer"
80
- ? "<number>"
81
- : p.schema?.type === "boolean"
82
- ? "<bool>"
83
- : "<value>"),
84
- })) ?? [],
85
- httpVersion: "",
86
- cookies: [],
87
- headersSize: 0,
88
- bodySize: 0,
89
- });
90
- };
91
-
92
- export const getConverted = (snippet: HTTPSnippet, option: string) => {
93
- // biome-ignore lint/suspicious/noExplicitAny: Allow any type
94
- let converted: any;
95
- switch (option) {
96
- case "shell":
97
- converted = snippet.convert("shell", "curl");
98
- break;
99
- case "js":
100
- converted = snippet.convert("javascript", "fetch");
101
- break;
102
- case "python":
103
- converted = snippet.convert("python", "requests");
104
- break;
105
- case "java":
106
- converted = snippet.convert("java", "okhttp");
107
- break;
108
- case "go":
109
- converted = snippet.convert("go", "native");
110
- break;
111
- case "csharp":
112
- converted = snippet.convert("csharp", "httpclient");
113
- break;
114
- case "kotlin":
115
- converted = snippet.convert("kotlin", "okhttp");
116
- break;
117
- case "objc":
118
- converted = snippet.convert("objc", "nsurlsession");
119
- break;
120
- case "php":
121
- converted = snippet.convert("php", "http2");
122
- break;
123
- case "ruby":
124
- converted = snippet.convert("ruby");
125
- break;
126
- case "swift":
127
- converted = snippet.convert("swift");
128
- break;
129
- default:
130
- converted = snippet.convert("shell");
131
- break;
132
- }
133
-
134
- return converted ? converted[0] : "";
135
- };
1
+ import { HTTPSnippet } from "@zudoku/httpsnippet";
2
+ import type { OperationsFragmentFragment } from "../graphql/graphql.js";
3
+
4
+ /** Operation extension with the header a key goes in (same name as `KEY_HEADER_EXTENSION` in the CLI's config). */
5
+ export const KEY_HEADER_EXTENSION = "x-apitogo-key-header";
6
+
7
+ /** What a code sample shows where the visitor's key goes. */
8
+ export const KEY_PLACEHOLDER = "YOUR_API_KEY";
9
+
10
+ const toFormDataParams = (text?: string) => {
11
+ const stringify = (v: unknown) =>
12
+ typeof v === "string" ? v : JSON.stringify(v);
13
+
14
+ try {
15
+ const obj = text && JSON.parse(text);
16
+ if (typeof obj !== "object" || !obj) return [];
17
+
18
+ return Object.entries(obj).flatMap(([name, value]) => {
19
+ const values = Array.isArray(value) ? value : [value];
20
+ return values.map((v) => ({ name, value: stringify(v) }));
21
+ });
22
+ } catch {
23
+ return [];
24
+ }
25
+ };
26
+
27
+ export const createHttpSnippet = ({
28
+ operation,
29
+ selectedServer,
30
+ exampleBody,
31
+ }: {
32
+ operation: OperationsFragmentFragment;
33
+ selectedServer: string;
34
+ exampleBody: {
35
+ mimeType: string;
36
+ text?: string;
37
+ };
38
+ }) => {
39
+ const isMultipart =
40
+ exampleBody.mimeType === "multipart/form-data" ||
41
+ exampleBody.mimeType === "application/x-www-form-urlencoded";
42
+
43
+ const postData = isMultipart
44
+ ? {
45
+ mimeType: exampleBody.mimeType,
46
+ params: toFormDataParams(exampleBody.text),
47
+ }
48
+ : exampleBody;
49
+
50
+ const keyHeader = operation.extensions?.[KEY_HEADER_EXTENSION];
51
+
52
+ return new HTTPSnippet({
53
+ method: operation.method.toUpperCase(),
54
+ url:
55
+ selectedServer + operation.path.replaceAll("{", ":").replaceAll("}", ""),
56
+ postData,
57
+ headers: [
58
+ // The gateway's key header, on operations that need a key (set by the portal build, API-120).
59
+ ...(typeof keyHeader === "string" && keyHeader
60
+ ? [{ name: keyHeader, value: KEY_PLACEHOLDER }]
61
+ : []),
62
+ ...(exampleBody.text
63
+ ? [{ name: "Content-Type", value: exampleBody.mimeType }]
64
+ : []),
65
+ ...(operation.parameters
66
+ ?.filter((p) => p.in === "header" && p.required === true)
67
+ .map((p) => ({
68
+ name: p.name,
69
+ value:
70
+ p.schema?.default ??
71
+ p.examples?.find((x) => x.value)?.value ??
72
+ (p.schema?.type === "string"
73
+ ? "<string>"
74
+ : p.schema?.type === "number" || p.schema?.type === "integer"
75
+ ? "<number>"
76
+ : p.schema?.type === "boolean"
77
+ ? "<bool>"
78
+ : "<value>"),
79
+ })) ?? []),
80
+ ],
81
+ queryString:
82
+ operation.parameters
83
+ ?.filter((p) => p.in === "query" && p.required === true)
84
+ .map((p) => ({
85
+ name: p.name,
86
+ value:
87
+ p.schema?.default ??
88
+ p.examples?.find((x) => x.value)?.value ??
89
+ (p.schema?.type === "string"
90
+ ? "<string>"
91
+ : p.schema?.type === "number" || p.schema?.type === "integer"
92
+ ? "<number>"
93
+ : p.schema?.type === "boolean"
94
+ ? "<bool>"
95
+ : "<value>"),
96
+ })) ?? [],
97
+ httpVersion: "",
98
+ cookies: [],
99
+ headersSize: 0,
100
+ bodySize: 0,
101
+ });
102
+ };
103
+
104
+ export const getConverted = (snippet: HTTPSnippet, option: string) => {
105
+ // biome-ignore lint/suspicious/noExplicitAny: Allow any type
106
+ let converted: any;
107
+ switch (option) {
108
+ case "shell":
109
+ converted = snippet.convert("shell", "curl");
110
+ break;
111
+ case "js":
112
+ converted = snippet.convert("javascript", "fetch");
113
+ break;
114
+ case "python":
115
+ converted = snippet.convert("python", "requests");
116
+ break;
117
+ case "java":
118
+ converted = snippet.convert("java", "okhttp");
119
+ break;
120
+ case "go":
121
+ converted = snippet.convert("go", "native");
122
+ break;
123
+ case "csharp":
124
+ converted = snippet.convert("csharp", "httpclient");
125
+ break;
126
+ case "kotlin":
127
+ converted = snippet.convert("kotlin", "okhttp");
128
+ break;
129
+ case "objc":
130
+ converted = snippet.convert("objc", "nsurlsession");
131
+ break;
132
+ case "php":
133
+ converted = snippet.convert("php", "http2");
134
+ break;
135
+ case "ruby":
136
+ converted = snippet.convert("ruby");
137
+ break;
138
+ case "swift":
139
+ converted = snippet.convert("swift");
140
+ break;
141
+ default:
142
+ converted = snippet.convert("shell");
143
+ break;
144
+ }
145
+
146
+ return converted ? converted[0] : "";
147
+ };