You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Accept empty-string query params (?category=, ?platform=, ?artifact=, ?hash=) on the public Registry Search and Registry Integrity endpoints, treating them as "no filter / no specific value" — matching the runtime code already in place and the precedent set by the public Jobs schema.
Why this matters
The website API contracts declare these four query params as .optional().default(""), which signals "absent or empty is fine". The runtime handlers are built around that contract:
apps/web/src/lib/api/registry-search-filters.ts:62 short-circuits matchesPlatform when platform is empty.
apps/web/src/lib/api/registry-search-filters.ts:103 short-circuits the category predicate when filters.category is empty.
apps/web/src/app/api/registry/integrity/route.ts destructures { artifact = "", hash = "" } and emits "snapshot" status when artifact is empty, mapping empty back to null in the JSON body.
But the Zod schemas in apps/web/src/lib/api/contracts.ts apply .regex(...) BEFORE .optional().default(""). Because default(...) only fires when the field is undefined, an explicit empty string ?platform= reaches the regex and gets rejected with HTTP 400 invalid_payload. That is exactly what an HTML form posts when filter inputs are left blank, what Raycast emits between filter resets, and what existing API clients that round-trip platform: null → "" send today. The other public list schema in the same file — publicJobsQuerySchema at line 118 — explicitly accepts "" (z.enum(["all", "true", "false", ""])), proving the maintainer intent is empty == no-filter for public query params.
This is a small contract violation with a real user impact: every blank-field form submission against /api/registry/search returns a 400 today.
GET /api/registry/search?q=foo&platform= → 400 invalid_payload, Zod issue path ["platform"]
GET /api/registry/search?q=foo&category= → 400 invalid_payload, Zod issue path ["category"]
GET /api/registry/integrity?artifact= → 400 invalid_payload, Zod issue path ["artifact"]
GET /api/registry/integrity?hash= → 400 invalid_payload, Zod issue path ["hash"]
Note that the runtime in both routes already handles empty correctly when the param is absent — the Zod gate is the only thing blocking explicit empty.
Desired behavior
Each of the four schemas should accept "" as a valid input (with the existing regex still gating non-empty values), matching the runtime semantics. The OpenAPI advertised default: "" becomes a real default both when the param is absent and when it is an explicit empty string. The four endpoints respond with 200:
Search treats empty category / platform as "no filter on this dimension" (already implemented in matchesPlatform / entryMatchesFilters).
Integrity treats empty artifact / hash as "snapshot listing" (already implemented in the route handler).
Non-empty inputs continue to be regex-validated exactly as today (no relaxation of the format checks).
Scope
apps/web/src/lib/api/contracts.ts — modify only the four schemas listed above. Keep safeSlugSchema untouched (it is also used as a path-param validator at lines 281, 282, 450, 522, 1156, 1165, where empty IS invalid).
tests/registry-search-api.test.ts — extend existing describe block with empty-param coverage.
tests/registry-integrity-api.test.ts — extend with empty artifact= / hash= coverage.
Out of scope
safeSlugSchema itself (path-param use must stay strict).
Any other Zod schema in contracts.ts.
The OpenAPI YAML at cloudflare/api-schema-heyclaude-openapi.yaml (generated; let the generator re-emit on a normal release cycle — schema shape is unchanged in semantics).
Generated artifacts under apps/web/src/generated/** and apps/web/public/data/**.
Any changes to publicJobsQuerySchema (it already does the right thing).
Acceptance criteria
PR includes Closes #<issue>.
GET /api/registry/search?q=fixture&platform= returns 200 and treats platform as no-filter.
GET /api/registry/search?q=fixture&category= returns 200 and treats category as no-filter.
GET /api/registry/integrity?artifact= returns 200 with status: "snapshot" and artifact: null in the body.
GET /api/registry/integrity?hash= returns 200 with the existing snapshot/match/mismatch logic, treating empty hash as "no hash provided".
Existing rejection of malformed non-empty values is unchanged (e.g. ?platform=%21bad still returns 400).
pnpm exec vitest run tests/registry-search-api.test.ts tests/registry-integrity-api.test.ts tests/api-contracts.test.ts passes.
Quality evidence required in the PR
No visual impact (API-only fix; no UI / page changes).
Before/after curl traces for each of the four endpoints showing 400 → 200 on empty inputs and 400 → 400 preserved on malformed non-empty inputs.
Test diff in PR description listing the added cases and the pattern used (z.union([z.literal(""), ...regex...]) or equivalent).
Backward-compatibility note: the public response shape and status codes for absent / valid params are unchanged; only the previously-400 explicit-empty path becomes a normal 200.
Goal
Accept empty-string query params (
?category=,?platform=,?artifact=,?hash=) on the public Registry Search and Registry Integrity endpoints, treating them as "no filter / no specific value" — matching the runtime code already in place and the precedent set by the public Jobs schema.Why this matters
The website API contracts declare these four query params as
.optional().default(""), which signals "absent or empty is fine". The runtime handlers are built around that contract:apps/web/src/lib/api/registry-search-filters.ts:62short-circuitsmatchesPlatformwhenplatformis empty.apps/web/src/lib/api/registry-search-filters.ts:103short-circuits thecategorypredicate whenfilters.categoryis empty.apps/web/src/app/api/registry/integrity/route.tsdestructures{ artifact = "", hash = "" }and emits"snapshot"status whenartifactis empty, mapping empty back tonullin the JSON body.But the Zod schemas in
apps/web/src/lib/api/contracts.tsapply.regex(...)BEFORE.optional().default(""). Becausedefault(...)only fires when the field isundefined, an explicit empty string?platform=reaches the regex and gets rejected withHTTP 400 invalid_payload. That is exactly what an HTML form posts when filter inputs are left blank, what Raycast emits between filter resets, and what existing API clients that round-tripplatform: null → ""send today. The other public list schema in the same file —publicJobsQuerySchemaat line 118 — explicitly accepts""(z.enum(["all", "true", "false", ""])), proving the maintainer intent is empty == no-filter for public query params.This is a small contract violation with a real user impact: every blank-field form submission against
/api/registry/searchreturns a 400 today.Current behavior
apps/web/src/lib/api/contracts.tsplatformSchema(lines 7–13):z.string().trim().toLowerCase().regex(/^[a-z0-9][a-z0-9 -]{0,48}$/).optional().default("")categorySchema(line 14):safeSlugSchema.optional().default("")wheresafeSlugSchema = z.string().regex(/^[a-z0-9-]+$/)registryIntegrityQuerySchema.artifact(line 276):z.string().trim().max(160).regex(/^\/?(?:[a-z0-9][a-z0-9._-]*\/)*(?:[a-z0-9][a-z0-9._-]*)$/).optional()registryIntegrityQuerySchema.hash(line 277):z.string().trim().toLowerCase().regex(/^[a-f0-9]{64}$/).optional()GET /api/registry/search?q=foo&platform=→ 400invalid_payload, Zod issue path["platform"]GET /api/registry/search?q=foo&category=→ 400invalid_payload, Zod issue path["category"]GET /api/registry/integrity?artifact=→ 400invalid_payload, Zod issue path["artifact"]GET /api/registry/integrity?hash=→ 400invalid_payload, Zod issue path["hash"]Desired behavior
Each of the four schemas should accept
""as a valid input (with the existing regex still gating non-empty values), matching the runtime semantics. The OpenAPI advertiseddefault: ""becomes a real default both when the param is absent and when it is an explicit empty string. The four endpoints respond with200:category/platformas "no filter on this dimension" (already implemented inmatchesPlatform/entryMatchesFilters).artifact/hashas "snapshot listing" (already implemented in the route handler).Scope
apps/web/src/lib/api/contracts.ts— modify only the four schemas listed above. KeepsafeSlugSchemauntouched (it is also used as a path-param validator at lines 281, 282, 450, 522, 1156, 1165, where empty IS invalid).tests/registry-search-api.test.ts— extend existing describe block with empty-param coverage.tests/registry-integrity-api.test.ts— extend with emptyartifact=/hash=coverage.Out of scope
safeSlugSchemaitself (path-param use must stay strict).contracts.ts.cloudflare/api-schema-heyclaude-openapi.yaml(generated; let the generator re-emit on a normal release cycle — schema shape is unchanged in semantics).apps/web/src/generated/**andapps/web/public/data/**.publicJobsQuerySchema(it already does the right thing).Acceptance criteria
Closes #<issue>.GET /api/registry/search?q=fixture&platform=returns200and treatsplatformas no-filter.GET /api/registry/search?q=fixture&category=returns200and treatscategoryas no-filter.GET /api/registry/integrity?artifact=returns200withstatus: "snapshot"andartifact: nullin the body.GET /api/registry/integrity?hash=returns200with the existing snapshot/match/mismatch logic, treating emptyhashas "no hash provided".?platform=%21badstill returns400).pnpm exec vitest run tests/registry-search-api.test.ts tests/registry-integrity-api.test.ts tests/api-contracts.test.tspasses.Quality evidence required in the PR
400 → 200on empty inputs and400 → 400preserved on malformed non-empty inputs.z.union([z.literal(""), ...regex...])or equivalent).Validation