Repository navigation
Describe the API with OpenAPI: /openapi.json and /docs - #11
Merged
Merged
Conversation
… §0) - pnpm project with dev/deploy/test/typecheck/lint/format/check/db:migrate:local scripts - wrangler.jsonc with the DB binding, CREATE_LIMITER/WRITE_LIMITER, hourly cron and Instance vars - Env type generated by `wrangler types` (`pnpm typegen`), so @cloudflare/workers-types isn't needed - src/config.ts parses vars with zod, applying defaults and rejecting MAX_BIN_BYTES > 1,000,000 - GET /health, tests for /health and config parsing, Biome, CI workflow Tests use @cloudflare/vitest-plugin, the renamed and maintained @cloudflare/vitest-pool-workers (the old name is deprecated on npm). AGENTS.md and TODO.md now name the new package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
- migrations/0001_bins.sql: the schema drafted in docs/api.md → Storage. - Tests apply the migrations to the local D1 through a vitest setup file (readD1Migrations in vitest.config.ts, applyD1Migrations in the worker). - src/bins/repository.ts holds all SQL: insert, findLive (leaves out expired Bins), replaceContent (live Bins only), remove, and deleteExpired(now, limit) for the Expiry sweep. - src/bins/keys.ts: Bin ids (nanoid), writeKey (kj_w_) and readKey (kj_r_) generation, hex SHA-256 hashKey and a constant-time matches. - Unit tests run the repository against the real local D1 and check key format, prefixes and a known SHA-256 vector. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
- parseBinBody(request, config) in src/bins/body.ts validates in the
api.md order: 415 unsupported_media_type, 413 payload_too_large
(Content-Length first, then the bytes actually read, stopping as soon
as the limit is passed), 400 invalid_json (including invalid UTF-8),
422 invalid_top_level. It returns the body text exactly as received,
so PUT (§5) can reuse it.
- POST /bins stores the content and the writeKey hash through the
repository and returns 201 with Location, the X-Bin-* metadata headers
and { id, writeKey, private, createdAt, expiresAt }. Bins are public
with no Expiry until §4 and §7.
- Shared pieces: ApiError and errorResponse (src/errors.ts), the config
middleware (src/context.ts), binMetadataHeaders (src/bins/metadata.ts),
Cache-Control: no-store on every response, and the error shape for
unknown routes.
- parseConfig now takes any object so it accepts the Worker's Env.
Contract change: docs/api.md gains a 500 internal_error row. Unexpected
errors still need the documented error shape, and the table had no code
for them; the details are neither returned nor logged because a request
can carry a writeKey.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
- GET /bins/:id returns the stored content byte-for-byte with Content-Type: application/json; charset=utf-8, the X-Bin-* metadata headers and Cache-Control: no-store. - findLiveBin(db, id, now) in src/bins/routes.ts answers 404 not_found for a malformed id (checked with isValidBinId before D1 is asked), an unknown id or an expired Bin. PUT (§5) and DELETE (§6) can reuse it, and §4 adds the Private Bin check right after it in the GET handler. - Tests: create → read round trips (object, array, empty, unicode, whitespace, key order, number formats, raw bytes), headers matching the create response with no X-Bin-Expires-At, the writeKey never sent, and 404s for unknown and malformed ids, including a malformed-id row inserted straight into D1 to prove it is never looked up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
X-Bin-Private: true on POST /bins makes a Private Bin: a kj_r_ Read Key is returned once as readKey and only its SHA-256 is stored in read_key_hash. Only the value "true" (any case, trimmed) counts; any other value makes a public Bin, since the contract has no error code for this header. GET /bins/:id on a Private Bin now needs Authorization: Bearer with the Read Key or the Write Key (401 unauthorized without a key, 403 forbidden with a wrong one). A public Bin ignores the Authorization header. The new requireKey(c, bin, "read" | "write") helper in src/bins/auth.ts compares keys in constant time, and PUT/DELETE can reuse it with "write". docs/api.md: clarify (no change in meaning) that only "true" makes a Private Bin and any other X-Bin-Private value makes a public one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
TODO §5. PUT needs the Write Key and checks, in the order of docs/api.md,
the id (404 for malformed, unknown or expired), then auth (401 without a
key, 403 for a Read Key or a wrong key), then the body via parseBinBody
(415, 413, 400, 422). The body is not read before auth passes.
The content is replaced through the repository: updatedAt moves forward
while createdAt, privacy and Expiry stay as they were; X-Bin-Private and
X-Bin-TTL are ignored. If the Bin disappears or expires between the
lookup and the update, the response is 404. The 200 response carries the
metadata headers and {id, private, createdAt, updatedAt, expiresAt}, and
never a key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
TODO §6. DELETE needs the Write Key and checks, in the order of docs/api.md, the id (404 for malformed, unknown or expired, before auth), then auth (401 without a key, 403 for a Read Key or a wrong key). The Bin is then removed through the repository and the response is 204 with no body. If another request deleted the Bin between the lookup and the removal, the response is 404. The Bin is gone immediately: a later GET or PUT gets 404, and a second DELETE gets 404. Other Bins are untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
TODO §7. POST /bins resolves the Expiry from X-Bin-TTL and the Instance's default/maximum TTL (src/bins/expiry.ts resolveExpiresAt), returns it as expiresAt and X-Bin-Expires-At, and rejects a bad TTL with 400 invalid_ttl. The scheduled handler sweeps expired Bins in batches of 500 until none are left or 20 s have passed (sweepExpired). remove now takes `now` and deletes only live Bins, so a Bin that expires between the lookup and the DELETE is a 404 rather than a 204. docs/api.md changes: the contract didn't say where X-Bin-TTL sits in the validation order or what exactly counts as a positive integer. X-Bin-TTL is now checked right after auth, before the content type and the body, since it is a header and can be rejected without reading the body; the accepted syntax (ASCII digits only) and the too-large case are spelled out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
TODO §8. CREATE_LIMITER guards POST /bins and WRITE_LIMITER guards PUT and
DELETE /bins/:id, keyed on CF-Connecting-IP. Requests without that header
share one fallback bucket ("unknown") instead of going unlimited. Over the
limit the response is 429 rate_limited with Retry-After set to the
bindings' 60 s period, since a binding can't report when its window
resets. RATE_LIMIT_ENABLED=false skips the limiter call entirely.
Contract change in docs/api.md: the rate limit is now the first step of
the validation order, ahead of route/id, auth and body checks. It exists
to protect the Worker, so a limited client should not cost a D1 lookup,
key hashing or body parsing. The Rate limiting section also documents
Retry-After and the missing-IP fallback key.
The test worker now runs with RATE_LIMIT_ENABLED=false (vitest.config.ts),
because the real Miniflare CREATE_LIMITER would start rejecting the many
Bins other test files create. test/rate-limit.test.ts calls the app
directly with stub limiter bindings and rate limiting turned on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
TODO §9. Hono's cors() runs at the app level, after the config middleware (now also app-level so the origin check reads the parsed CORS_ORIGINS for each request) and before the routes. So /health, 404s, onError responses and 429s all carry the CORS headers, and preflights are answered with 204 before the rate limiter or D1. `*` sends ACAO *; an allowlist echoes the Origin only on an exact match. No Access-Control-Allow-Credentials, since keys go in the Authorization header. Contract change in docs/api.md: Retry-After is added to Access-Control-Expose-Headers. It isn't a CORS-safelisted response header, so without it a browser couldn't read how long to wait after a 429. The CORS section now also spells out the allowlist matching, Vary: Origin, the exposed headers, no credentials and the preflight behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
TODO §10. Every route (GET /health, POST /bins, GET/PUT/DELETE /bins/:id) is
now registered with createRoute + app.openapi, describing its request headers,
the id path parameter, the JSON body (any object or array), each success
response with its headers and body, and every error response from
docs/api.md through one shared Error schema whose code is an enum of the
error codes. PUT and DELETE require the bearerAuth scheme; GET /bins/{id}
takes it optionally, since a public Bin needs no key.
Behaviour doesn't change: request headers, the id and the body are described
as plain OpenAPI objects rather than Zod schemas, so zod-openapi adds no
validators. parseBinBody, resolveExpiresAt, requireKey and the 404 for a
malformed id keep their order and error codes, and the rate limiter is still
registered ahead of each handler. Authorization and Content-Type are
described through the security scheme and the request body rather than as
header parameters, which OpenAPI 3.1 says to ignore.
GET /openapi.json serves the OpenAPI 3.1 spec and GET /docs a Swagger UI
page loaded from jsDelivr at a pinned version with SRI. docs/api.md → Routes
lists both endpoints, because they are new public routes of the contract.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
Review feedback on #9: registering rateLimit() as a separate route before the handler works, but the idiomatic @hono/zod-openapi form is the route's own middleware option, which keeps the limiter next to the route it guards. Behaviour is unchanged: the limiter still runs before the id, auth and body checks, as the rate-limit tests verify. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
…erged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
avinoth
changed the base branch from
claude/keepjson-tasks-zdcoz5-task9
to
main
October 10, 2026 02:16
avinoth
commented
Oct 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
docs/TODO.md§10. All its boxes are ticked. Stacked on #10: merge the earlier PRs first, in order.Changes
createRoute+.openapi(...):GET /health,POST /bins, andGET,PUTandDELETE /bins/:id. Definitions live insrc/bins/spec.tsandsrc/openapi.ts.Location,X-Bin-*,Retry-After).{ error: { code, message } }schema whosecodeenum covers the whole table in api.md.bearerAuthscheme, required on PUT and DELETE and optional on GET.GET /openapi.json: serves the OpenAPI 3.1 spec.GET /docs: a Swagger UI page that loadsswagger-ui-dist@5.32.15from jsDelivr with SRI hashes. No new runtime dependency.docs/api.md: lists the two new routes.Behaviour is unchanged
Request headers, the
idparam and the request body are described as plain OpenAPI objects, not Zod schemas. So zod-openapi adds no validators:parseBinBody,resolveExpiresAt,requireKeyand the 404 for malformed ids keep their order and error codes. The rate-limit middleware still runs first. All 276 earlier tests pass unchanged.Notes
AuthorizationandContent-Typearen't listed as header parameters, because OpenAPI 3.1 ignores those. They're described through the security scheme and the request body instead.swagger-ui-dist@5.32.15, which jsDelivr serves unchanged. I couldn't reach the CDN from the sandbox to confirm directly, so please open/docsonce after deploying.Tests
25 new tests:
$refresolves, the response headers, params, bodies and security./openapi.jsonand/docssend CORS and no-store headers. They are tested with D1 and the limiters set to throw, so a call to either would fail the test.Verification
pnpm checkpasses: typecheck, lint, and 301 tests.🤖 Generated with Claude Code
https://claude.ai/code/session_01NK16ZSCdWC5s34GE9xpLcX
Generated by Claude Code