Skip to content
30 changes: 8 additions & 22 deletions apps/docs/openapi/public.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ actions:
input_format: png
target_format: webp
input_bytes: 48213
- target: $.paths['/v1/jobs'].post.requestBody.content['application/json'].schema.properties.input_format
- target: $.components.schemas.CreateJob.properties.input_format
update:
description: Format id of the file you upload, such as `png` or `docx`. See [Formats](https://convt.app/docs/reference/formats).
- target: $.paths['/v1/jobs'].post.requestBody.content['application/json'].schema.properties.target_format
- target: $.components.schemas.CreateJob.properties.target_format
update:
description: Format id to convert to. It must differ from `input_format` and be a target the input can reach.
- target: $.paths['/v1/jobs'].post.requestBody.content['application/json'].schema.properties.input_bytes
- target: $.components.schemas.CreateJob.properties.input_bytes
update:
description: Exact size of the upload in bytes.
- target: $.paths['/v1/jobs'].post.responses['200']
Expand All @@ -68,10 +68,10 @@ actions:
expires_at: "2026-10-08T12:00:00Z"
upload_url: https://bucket.example/job_01k6z7v4q8m3x2a9b5c0d1e2f3/upload?X-Amz-Signature=…
upload_expires_in: 900
- target: $.paths['/v1/jobs'].post.responses['200'].content['application/json'].schema.properties.upload_url
- target: $.components.schemas.JobReservation.properties.upload_url
update:
description: Signed URL. Send the file bytes to it with `PUT`.
- target: $.paths['/v1/jobs'].post.responses['200'].content['application/json'].schema.properties.upload_expires_in
- target: $.components.schemas.JobReservation.properties.upload_expires_in
update:
description: Seconds until `upload_url` stops accepting uploads. Always 900.
- target: $.paths['/v1/jobs'].post.responses['400']
Expand Down Expand Up @@ -225,26 +225,12 @@ actions:
extensions: [webp]
mime: image/webp

# Shared
- target: $.paths.*.*.parameters[?@.name == 'id']
# Shared. The 401, 403, 404, 429, 500 and 502 responses every job route shares are
# components in the spec and carry their own descriptions.
- target: $.components.parameters.JobId
update:
description: The job id returned when you created the job, such as `job_01k6z7v4q8m3x2a9b5c0d1e2f3`.
example: job_01k6z7v4q8m3x2a9b5c0d1e2f3
- target: $.paths.*.*.responses['401']
update:
description: "`unauthorized`: no `Authorization: Bearer` header."
- target: $.paths.*.*.responses['403']
update:
description: "`unauthorized` (key invalid or revoked) or a billing limit."
- target: $.paths.*.*.responses['404']
update:
description: "`not_found`: the job does not exist, belongs to another account, or has expired."
- target: $.paths.*.*.responses['429']
update:
description: "`rate_limited`: more than 120 requests in a minute for this key."
- target: $.paths.*.*.responses['502']
update:
description: "`storage_unavailable`: object storage did not answer. Retry with backoff."
- target: $.components.securitySchemes.bearerAuth
update:
description: "A `cvt_live_` API key from the [dashboard](https://convt.app/dashboard/api), sent as `Authorization: Bearer cvt_live_…`. Keep it on your server."
Expand Down
35 changes: 35 additions & 0 deletions apps/docs/scripts/check-overlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Fails the build when an action in openapi/public.yaml targets nothing in the spec.
// Blume skips unmatched targets silently, so a spec refactor (an inline schema moving
// to components, say) would otherwise drop the overlay's prose without a warning.
// Targets are plain paths: `$`, `.name` and `['name']`. Wildcards and filters are
// refused because they can match nothing without being wrong.

const spec = await Bun.file(
new URL("../../../crates/convt-server/openapi.json", import.meta.url),
).json();
const overlay = Bun.YAML.parse(
await Bun.file(new URL("../openapi/public.yaml", import.meta.url)).text(),
) as { actions: { target: string }[] };

const segment = /\.([A-Za-z_$][\w$-]*)|\['([^']+)'\]/y;

function resolve(target: string): unknown {
if (!target.startsWith("$")) throw new Error(`${target}: targets start with $`);
let value: unknown = spec;
segment.lastIndex = 1;
while (segment.lastIndex < target.length) {
const at = segment.lastIndex;
const match = segment.exec(target);
if (!match) throw new Error(`${target}: unsupported syntax at "${target.slice(at)}"`);
value = (value as Record<string, unknown> | undefined)?.[match[1] ?? match[2]];
}
return value;
}

const unmatched = overlay.actions
.map((a) => a.target)
.filter((target) => resolve(target) === undefined);
if (unmatched.length) {
throw new Error(`openapi/public.yaml targets nothing in the spec:\n ${unmatched.join("\n ")}`);
}
console.log(`public.yaml: ${overlay.actions.length} overlay targets match the spec`);
Comment on lines +29 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Overlay check has a narrow guarantee

resolve verifies target existence, not whether updates appear in the rendered reference. An overlay-output check is needed to catch application failures.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

1 change: 1 addition & 0 deletions apps/docs/scripts/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ actions:

await Bun.write(new URL("../openapi/servers.yaml", import.meta.url), servers);
await import("./generate-formats.ts");
await import("./check-overlay.ts");
Loading
Loading