block 1: critical ship-blockers (upload cap, auth hardening, bounded workq, scoped search)#60
Merged
block 1: critical ship-blockers (upload cap, auth hardening, bounded workq, scoped search)#60
Conversation
New server.max_upload_bytes config (default 100 MiB, env DOCSIQ_SERVER_MAX_UPLOAD_BYTES). Upload handler wraps request body with http.MaxBytesReader before ParseMultipartForm so oversized requests terminate at the transport with 413 instead of allocating memory or temp files. Closes the P2-1 TODO in handlers.go. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop double WriteHeader in *http.MaxBytesError branch (MaxBytesReader already committed the 413) - Test asserts JSON contract + Content-Type header - New TestUploadMaxBytes_UnknownContentLength covers the chunked/unknown-length slow path - Extract writeTooLarge helper (DRY the 413 body) - Comment limit<=0 escape hatch on the config field - slog.Warn on oversize reject (matches repo convention) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
validateServeSecurity fails startup when server.api_key is empty AND server.host is not loopback. Loopback+empty emits a prominent slog warning instead. Closes the "auth-disabled-by-default" ship-blocker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Refuse empty server.host (binds all interfaces, not loopback) - Use net.ParseIP().IsLoopback() to accept [::1], 127.0.0.0/8, and ::ffff:127.0.0.1 as loopback - Table-driven tests cover new hosts + empty-string case - Assert DOCSIQ_SERVER_API_KEY is named in the refusal message - gofmt Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New internal/workq package. Pool has fixed worker count and a bounded submission queue; Submit is non-blocking and returns ErrQueueFull when saturated. Close(ctx) cancels the pool context and waits for workers to drain within the caller's deadline. Preparing to replace fire-and-forget upload indexing goroutine. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RWMutex guards the jobs-channel send in Submit against a concurrent Close that calls close(p.jobs). Previously a Submit that passed the p.closed check could be preempted and then send on an already-closed channel. Add a stress test (50 iterations x 32 concurrent Submits + Close) to exercise the race under `-race`. Modernize the iteration count in TestPool_CloseDrainsInflight to Go 1.22's for-range-N form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upload handler now submits the indexing closure to a bounded workq Pool instead of spawning a detached goroutine. When the queue is full the handler returns 503 with Retry-After: 30. On shutdown, cmd/serve calls pool.Close with a 30s deadline so in-flight jobs honour the cancelled context and partial writes are avoided. Config: server.workq_workers (default NumCPU), server.workq_depth (default 64). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- upload(): set progress to rejected state before writing 503 when workq.Submit returns ErrQueueFull or ErrClosed, so progress polling reflects the rejection instead of stale "queued" forever. - upload_workq_test.go: defer close(block) after defer pool.Close so drain completes even if the test panics. Also add started-channel sync and fill both channel slots (capacity = Workers+QueueDepth = 2) so saturation is deterministic under the race detector. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New POST /api/session exchanges an Authorization: Bearer key for a docsiq_session httpOnly; Secure; SameSite=Strict cookie (30-day Max-Age). DELETE /api/session clears it. Auth middleware now accepts either the Authorization header or the cookie; /api/session itself is public (it is the auth boundary). Preparing to stop injecting the key into served HTML. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- bearerAuthMiddleware: explicitly reject when server.api_key is unset, matching newSessionHandler's guard. Previously correct only via the no_token branch firing first; fragile under refactor. - extractToken: trim whitespace from the session cookie value for parity with the Authorization header path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apiFetch now sends credentials: 'include' on every request; the Authorization header is no longer attached. On boot, initAuth exchanges the legacy meta-tag bearer (if present) for a cookie via POST /api/session exactly once and scrubs the meta tag. Production installs that already ship cookies out-of-band (via `docsiq login`) skip the exchange entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Check res.ok on POST /api/session. Non-2xx rejects sessionReady so the UI sees a single clear auth-boundary failure instead of a cascade of generic 401s on subsequent fetches. - Scrub the legacy meta tag BEFORE awaiting the fetch. The bearer is already captured as a closure param; leaving the tag in the DOM during a slow/hung exchange widens the disclosure window. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The SPA handler no longer rewrites index.html to embed the bearer in a <meta name="docsiq-api-key"> tag. Browser auth now flows through the docsiq_session cookie set by POST /api/session. Closes the "API-key-in-HTML" ship-blocker. The stale spa_meta_test.go asserted the injection was present and is replaced by spa_test.go which asserts the tag is absent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the full-table AllEntities scan with EntitiesForDocs, which joins the entities → relationships tables and filters by relationships.doc_id with a chunked IN-list (chunk size 900, below SQLite's 999 variable limit). Local search now scales sub-linearly with corpus size. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useMCP had its own getBearer() reading the docsiq-api-key meta tag and attaching Authorization to /mcp requests — a parallel path that Tasks 5-7 missed. Now the hook sends credentials: 'include' and relies on the docsiq_session cookie, matching apiFetch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Closes all five Block 1 items from the production-polish roadmap (docs/superpowers/specs/2026-04-23-production-polish-roadmap-design.md). Ships via 16 commits organized as 8 tasks per the Block 1 plan (docs/superpowers/plans/2026-04-23-block1-critical-plan.md), each with its own spec-compliance + code-quality review gates.
Summary
http.MaxBytesReader+ Content-Length fast-path caps multipart uploads; configurable viaserver.max_upload_bytes(default 256 MiB).cmd/serve.gorefuses to start whenserver.api_keyis empty AND host binds non-loopback. Usesnet.ParseIP().IsLoopback()so[::1],127.0.0.0/8, and::ffff:127.0.0.1are all covered.internal/workqbounded pool (Workers + QueueDepthcapacity, send-on-closed-channel race guarded bysync.RWMutex); upload handler submits through the pool, returns 503 +Retry-After: 30when full; server shutdown drains with a 30s deadline aftersrv.Shutdown.POST /api/sessionexchanges bearer fordocsiq_session httpOnly; Secure; SameSite=Strictcookie (Max-Age 30d);DELETE /api/sessionclears it; auth middleware accepts header OR cookie; SPA handler no longer injects the<meta name=\"docsiq-api-key\">tag; UIapiFetchanduseMCP.rpcnow sendcredentials: 'include'and never attach Authorization.Store.EntitiesForDocs(ctx, docIDs)JOINs entities → relationships with chunked IN-lists (chunk size 900, below SQLite's 999 default);internal/search/local.gouses it instead ofAllEntities.Test plan
CGO_ENABLED=1 go test -tags sqlite_fts5 -timeout 300s ./...→ 541/541 pass across 23 packagesnpm test -- --run→ 54/54 pass across 18 filestsc --project tsconfig.app.json --noEmit→ cleanTestUploadMaxBytes{,_UnknownContentLength},TestValidateServeSecurity(12 subtests),TestPool_*(5 subtests incl.TestPool_SubmitRaceDuringClose50x32),TestUpload_ReturnsRetryOnFullQueue,TestSession_*(3),TestAuth_AcceptsValidCookie,TestAuth_RejectsMissingBothHeaderAndCookie,TestSpaHandler_DoesNotInjectAPIKey,TestEntitiesForDocs_*(2)Pre-merge review
Final integration review verdict:⚠️ Merge with documented follow-ups. Per-task reviews (spec + quality) passed for all 8 tasks; the one Important integration finding (dead meta-tag read in
useMCP.ts) was fixed inline inb7b1168.Known follow-ups (non-blocking, file after merge)
docsiq loginCLI is referenced in code comments / spec but doesn't exist yet. Operators on non-loopback hosts needcurl -X POST /api/session -H \"Authorization: Bearer …\"to seed the cookie for a fresh browser.Secure: truecookies are silently dropped by browsers over plain HTTP.go run . serveon HTTP localhost with an API key configured will leave the SPA unable to auth. Consider aserver.cookie_secureconfig override (default true) for TLS-off dev.docs/superpowers/plans/2026-04-18-ui-redesign.md) still describes the old meta-tag flow. Left as append-only history.Commits (16)
21 files, +1,326 / −90.
Blocks 2–7 will ship as separate PRs per the roadmap cadence.
🤖 Generated with Claude Code