diff --git a/astro.config.ts b/astro.config.ts index 271a4ebf2e164b..d9f6ef3f502980 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -149,9 +149,7 @@ export default defineConfig({ tailwind({ applyBaseStyles: false, }), - liveCode({ - layout: "~/components/live-code/Layout.astro", - }), + liveCode({}), icon(), sitemap({ filter(page) { diff --git a/package-lock.json b/package-lock.json index 3aecf039bd245f..057c18ad9ef7e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "devDependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.0", + "@apidevtools/swagger-parser": "10.1.1", "@astrojs/check": "0.9.4", "@astrojs/react": "4.2.0", "@astrojs/rss": "4.0.11", @@ -59,6 +60,7 @@ "mdast-util-mdx-expression": "2.0.1", "mermaid": "11.4.1", "node-html-parser": "7.0.1", + "openapi-types": "12.1.3", "parse-duration": "2.1.3", "prettier": "3.5.2", "prettier-plugin-astro": "0.14.1", @@ -464,6 +466,99 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", + "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz", + "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "11.7.2", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/@astrojs/check": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.4.tgz", @@ -2883,6 +2978,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, "node_modules/@marsidev/react-turnstile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-1.1.0.tgz", @@ -6398,6 +6500,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -13632,6 +13741,13 @@ "regex-recursion": "^5.1.1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", diff --git a/package.json b/package.json index 35868cedb4e095..155c0ad571e2ee 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "devDependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.0", + "@apidevtools/swagger-parser": "10.1.1", "@astrojs/check": "0.9.4", "@astrojs/react": "4.2.0", "@astrojs/rss": "4.0.11", @@ -77,6 +78,7 @@ "mdast-util-mdx-expression": "2.0.1", "mermaid": "11.4.1", "node-html-parser": "7.0.1", + "openapi-types": "12.1.3", "parse-duration": "2.1.3", "prettier": "3.5.2", "prettier-plugin-astro": "0.14.1", diff --git a/src/components/APIRequest.astro b/src/components/APIRequest.astro new file mode 100644 index 00000000000000..df39da642ac75e --- /dev/null +++ b/src/components/APIRequest.astro @@ -0,0 +1,124 @@ +--- +import { z } from "astro:schema"; +import { getProperty } from "dot-prop"; +import { getSchema } from "~/util/api.ts"; +import type { OpenAPIV3 } from "openapi-types"; +import CURL from "./CURL.astro"; +import Details from "./Details.astro"; + +type Props = z.input; + +const props = z.object({ + path: z.string(), + method: z.enum(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"]), + json: z.record(z.string(), z.any()).default({}), +}); + +const { path, method, json } = props.parse(Astro.props); + +const schema = await getSchema(); + +const operation = getProperty( + schema, + `paths.${path}.${method.toLowerCase()}`, +) as unknown as + | OpenAPIV3.OperationObject<{ + "x-api-token-group"?: string[]; + }> + | undefined; + +if (!operation) { + throw new Error( + `[APIRequest] Operation ${method} ${path} not found in schema.`, + ); +} + +const url = new URL(path, "https://api.cloudflare.com/client/v4"); +const headers: Record = {}; + +const segments = url.pathname.split("/").filter(Boolean); +for (const segment of segments) { + const decoded = decodeURIComponent(segment); + + if (decoded.startsWith("{") && decoded.endsWith("}")) { + const placeholder = "$" + decoded.slice(1, -1).toUpperCase(); + + url.pathname = url.pathname.replace(segment, placeholder); + } +} + +const security = operation.security as + | OpenAPIV3.SecurityRequirementObject[] + | undefined; + +if (security) { + const keys = security.flatMap((requirement) => Object.keys(requirement)); + + if (keys.includes("api_token")) { + headers["Authorization"] = `Bearer $CLOUDFLARE_API_TOKEN`; + } else if (keys.includes("api_key")) { + headers["X-Auth-Email"] = "$CLOUDFLARE_EMAIL"; + headers["X-Auth-Key"] = "$CLOUDFLARE_API_KEY"; + } +} + +const requestBody = operation?.requestBody as + | OpenAPIV3.RequestBodyObject + | undefined; + +const jsonSchema = requestBody?.content?.["application/json"]?.schema as + | OpenAPIV3.SchemaObject + | undefined; + +if (!jsonSchema) { + throw new Error( + `[APIRequest] This component currently does not support operations that do not accept JSON bodies.`, + ); +} + +if (jsonSchema.required) { + const providedProperties = Object.keys(json); + const requiredProperties = jsonSchema.required; + + const missingProperties = requiredProperties.filter( + (property) => !providedProperties.includes(property), + ); + + for (const property of missingProperties) { + const defaultValue = + (jsonSchema.properties?.[property] as OpenAPIV3.SchemaObject).default ?? + property; + + json[property] = defaultValue; + } +} + +const tokenGroups = operation["x-api-token-group"]; +--- + +{ + tokenGroups && ( +
+ + At least one of the following{" "} + token permissions{" "} + is required: + +
    + {tokenGroups.map((group) => ( +
  • {group}
  • + ))} +
+
+ ) +} + + diff --git a/src/components/CURL.astro b/src/components/CURL.astro new file mode 100644 index 00000000000000..605d52044983ef --- /dev/null +++ b/src/components/CURL.astro @@ -0,0 +1,38 @@ +--- +import { z } from "astro:schema"; +import type { ComponentProps } from "astro/types"; +import { Code } from "@astrojs/starlight/components"; + +type Props = z.input; + +const props = z.object({ + method: z + .enum(["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]) + .default("GET"), + url: z.string().url(), + headers: z.record(z.string(), z.string()).default({}), + json: z.record(z.string(), z.any()).optional(), + code: z + .custom, "code" | "lang">>() + .optional(), +}); + +const { method, url, headers, json, code } = props.parse(Astro.props); + +const lines = [`curl ${url}`, `\t--request ${method}`]; + +if (headers) { + for (const [key, value] of Object.entries(headers)) { + lines.push(`\t--header "${key}: ${value}"`); + } +} + +if (json) { + const jsonLines = JSON.stringify(json, null, "\t\t").split("\n"); + jsonLines[jsonLines.length - 1] = "\t" + jsonLines[jsonLines.length - 1]; + + lines.push(`\t--json '${jsonLines.join("\n")}'`); +} +--- + + diff --git a/src/components/index.ts b/src/components/index.ts index 76ae234db69b35..eea5b65c08f844 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,9 +6,11 @@ export { PackageManagers } from "starlight-package-managers"; export { Icon as AstroIcon } from "astro-icon/components"; // Custom components export { default as AnchorHeading } from "./AnchorHeading.astro"; +export { default as APIRequest } from "./APIRequest.astro"; export { default as AvailableNotifications } from "./AvailableNotifications.astro"; export { default as CompatibilityFlag } from "./CompatibilityFlag.astro"; export { default as CompatibilityFlags } from "./CompatibilityFlags.astro"; +export { default as CURL } from "./CURL.astro"; export { default as Description } from "./Description.astro"; export { default as Details } from "./Details.astro"; export { default as DirectoryListing } from "./DirectoryListing.astro"; diff --git a/src/components/live-code/Layout.astro b/src/components/live-code/Layout.astro deleted file mode 100644 index ed65e21802f926..00000000000000 --- a/src/components/live-code/Layout.astro +++ /dev/null @@ -1,27 +0,0 @@ -
-
-
- -
-
- -
- - diff --git a/src/content/docs/api-shield/security/schema-validation/configure.mdx b/src/content/docs/api-shield/security/schema-validation/configure.mdx index 555d4cc7744c29..ad98aab4e63a50 100644 --- a/src/content/docs/api-shield/security/schema-validation/configure.mdx +++ b/src/content/docs/api-shield/security/schema-validation/configure.mdx @@ -13,7 +13,7 @@ Schema Validation 2.0 allows all corresponding configuration calls to be made vi :::note -[Classic Schema Validation documentation](/api-shield/reference/classic-schema-validation/) is available for reference only. +[Classic Schema Validation documentation](/api-shield/reference/classic-schema-validation/) is available for reference only. ::: ## Upload schemas via the API to Schema Validation @@ -36,7 +36,7 @@ Settings changes may take a few minutes to implement. :::note -Endpoints must be listed in Endpoint Management for Schema Validation to match requests. +Endpoints must be listed in Endpoint Management for Schema Validation to match requests. ::: ## Configuration @@ -184,7 +184,7 @@ curl --silent "https://api.cloudflare.com/client/v4/zones/{zone_id}/api_gateway/ :::note -If you run this command again immediately, it will result in an error as all `new_operations` are now `existing_operations`. +If you run this command again immediately, it will result in an error as all `new_operations` are now `existing_operations`. ::: ### Change the default and operation-specific mitigation action @@ -318,7 +318,7 @@ curl "https://api.cloudflare.com/client/v4/zones/{zone_id}/api_gateway/user_sche :::note -We recommend using the query parameter `omit_source=true` to only display active schemas and not retrieve the source for every schema to get less output. +We recommend using the query parameter `omit_source=true` to only display active schemas and not retrieve the source for every schema to get less output. ::: ### Delete a schema @@ -414,7 +414,7 @@ curl --request PUT "https://api.cloudflare.com/client/v4/zones/{zone_id}/api_gat :::note -Parameter schemas are updated between every 24 hours up to one week. To ensure that a parameter schema has not been updated during the inspection, Cloudflare recommends that you pass the `last_updated` timestamp of the parameter-schema feature (not the `last_updated` of the whole operation) as an identifier in the timestamp query parameter. +Parameter schemas are updated between every 24 hours up to one week. To ensure that a parameter schema has not been updated during the inspection, Cloudflare recommends that you pass the `last_updated` timestamp of the parameter-schema feature (not the `last_updated` of the whole operation) as an identifier in the timestamp query parameter. ::: ### Disable Schema Validation diff --git a/src/content/docs/style-guide/components/api-request.mdx b/src/content/docs/style-guide/components/api-request.mdx new file mode 100644 index 00000000000000..a6887436906a45 --- /dev/null +++ b/src/content/docs/style-guide/components/api-request.mdx @@ -0,0 +1,51 @@ +--- +title: API request +--- + +## Import + +```mdx +import { APIRequest } from "~/components"; +``` + +## Usage + +```mdx live +import { APIRequest } from "~/components"; + + +``` + +## `` Props + +### `path` + +**required** + +**type:** `string` + +The path for the API endpoint. + +This can be found in our [API documentation](https://api.cloudflare.com), under the name of the endpoint. + +### `method` + +**required** + +**type:** `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` + +The HTTP method to use. + +### `json` + +**type:** `Record` + +The JSON payload to send. + +Default values can be omitted, and the component will use default values for any missing fields. diff --git a/src/content/docs/style-guide/components/curl.mdx b/src/content/docs/style-guide/components/curl.mdx new file mode 100644 index 00000000000000..75d9234baed239 --- /dev/null +++ b/src/content/docs/style-guide/components/curl.mdx @@ -0,0 +1,68 @@ +--- +title: CURL +--- + +The `CURL` component is used to display a cURL command for making HTTP requests. + +## Import + +```mdx +import { CURL } from "~/components"; +``` + +## Usage + +```mdx live +import { CURL } from "~/components"; + + +``` + +## `` Props + +### `url` + +**required** + +**type:** `string` + +The URL to make the request to. + +### `method` + +**type:** `"GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "PATCH"` + +**default:** `"GET"` + +The HTTP method to use for the request. + +### `headers` + +**type:** `Record` + +The headers to include in the request. + +### `json` + +**type:** `Record` + +JSON data to include in the request. + +### `code` + +**type:** `object` + +An object of Expressive Code props, the following props are available: + +- [Base Props](https://expressive-code.com/key-features/code-component/#available-props) +- [Line Marker Props](https://expressive-code.com/key-features/text-markers/#props) +- [Collapsible Sections Props](https://expressive-code.com/plugins/collapsible-sections/#props) \ No newline at end of file diff --git a/src/styles/live-code.css b/src/styles/live-code.css new file mode 100644 index 00000000000000..003a451b6c249a --- /dev/null +++ b/src/styles/live-code.css @@ -0,0 +1,5 @@ +.example-container { + display: block !important; + background-color: var(--sl-color-bg) !important; + border-color: var(--sl-color-hairline) !important; +} diff --git a/src/util/api.ts b/src/util/api.ts new file mode 100644 index 00000000000000..4e133e25a3dfec --- /dev/null +++ b/src/util/api.ts @@ -0,0 +1,17 @@ +import SwaggerParser from "@apidevtools/swagger-parser"; +import type { OpenAPI } from "openapi-types"; + +let schema: OpenAPI.Document | undefined; + +export const getSchema = async () => { + if (!schema) { + const response = await fetch( + "https://gh-code.developers.cloudflare.com/cloudflare/api-schemas/f6c9d752f31f2e9dea3a9659fefab1b97b6042e9/openapi.json", + ); + const obj = await response.json(); + + schema = await SwaggerParser.dereference(obj); + } + + return schema; +};