oficina / docs Public

signin-and-samples: magic-link sign-in, document ownership, sample content, live closed

Thirteen commits: magic-link sign-in, per-person document ownership, six sample documents, and the app live at https://docs.bds.oncarlos.com.

What this branch does

Sign-in. rastrillo/auth, magic link only, no passkey second factor. Spec §8.1 is reframed here: a single instance signing people in itself isn't merely acceptable, it's required by factor VII. Self-hosting is a right, so the standalone path can't be a degraded tier.

Ownership. Documents belong to people. owned() refuses owner <= 0 deliberately rather than trusting every call site, because the migration's DEFAULT 0 creates orphans and an unscoped query fails as a believable empty list rather than an error. scope.Owned is not used, for the same reason.

Sample documents. Six, 701 to 5,568 characters, in four recency buckets. Real English prose, not lorem ipsum — Latin filler hides the line-length and rhythm problems the canvas exists to get right. Their text exists only as replayed Yjs updates through WebAssembly; there is no content column. The .update.bin artefacts are honest output of real yjs with a pinned client id, byte-identical across runs.

The failure shape this branch kept finding

One defect recurred, in six places, and it is worth naming because it survives review by construction: a check that reports success while measuring something adjacent to the question.

  • TestSeedRequiresDevelopmentEnv re-implemented the gate expression and compared it to itself. It called nothing from main.go. Inverting the real if left it green.
  • carlos ship --target declares an architecture rather than checking it. An amd64 ELF labelled linux-arm64 shipped and promoted successfully; the box then failed to start it thirty times. Nothing compared the label to the file's e_machine.
  • The sign-in allowlist was tested only at admission, while the vulnerability was at send.
  • TestApplyUpdateDoesNotBumpAnotherOwnersDocument was named as proving a write was refused but asserted only that a timestamp held still.

The last one is the one to dwell on, because three things agreed with each other and all three were wrong: the code comment claimed it had closed "the one write in this file that skipped the seam" (there were two), the test's name certified a property it never checked, and my own verification note certified it as done. I had read the lines I was pointed at, seen owned(...) wrapping the timestamp, and generalised from a line to an operation.

The whole-branch review caught it. ApplyUpdate's content write — the doc_updates INSERT — carried no owner predicate, and because a gorm UPDATE matching zero rows is not an error, the method returned nil while a stranger's bytes landed in the victim's document. Reproduced directly: a forged OwnerID writes an entire document into someone else's and reports success.

Not reachable today — only Seed calls it, with a correct owner. But fix round 1's own comment had predicted that the editing plan would add a route calling ApplyUpdate and that whoever wrote it "would have copied this line as the established pattern." That is exactly what would have happened.

It now re-derives ownership from the database before anything is replayed or written, because a *Document is a value a caller can construct freely and a row is not.

The security fix, and how it was proven

rastrillo/auth consults Config.Authorize only at admission — handlers.go:131, after the link is clicked — and never in Begin. Ours was nil, and nil admits everybody. So any stranger reaching the instance could have had our SES identity mail them a link that worked, granting a session and a document space: unrestricted account creation on an instance-per-team app.

Found by the CARLOS Sheets session on their own deployed instance. Their diagnosis of why it survived review generalises: every unit test passed, because they all tested admission and none tested delivery.

Gated before Begin now, with byte-identical refusals so the endpoint can't be used to probe which addresses exist. Verified live against SES's own send counter rather than our test suite — the counter is an oracle outside our code, and our own tests could only ever assert what our own mailer recorded, which is precisely the blind spot that hid the bug:

stranger@example.com -> 303 ?sent=1, SES counter 1 -> 1, NO mail
paul@keymail.dev     -> 303 ?sent=1, SES counter 1 -> 2, delivered
bodies and headers byte-identical

An empty allowlist refuses at startup outside development rather than admitting nobody, because the security fix and the silent-misconfiguration failure mode are the same mechanism: an instance admitting nobody boots looking perfectly healthy, answering its ordinary 303 ?sent=1 on every submission because the byte-identical refusal is working as designed.

Deployed

Live at https://docs.bds.oncarlos.com, version f962e18, confirmed by asking the process (GET /api/version) rather than the router's x-carlos-version header, which reports the adopted release and not the running one. rastrillo.BuildVersion defaults to the literal "dev" unless stamped via ldflags — without the stamp that check runs, passes, and distinguishes nothing.

Reviewed as a single diff by the highest-capability model per AGENTS.md. Full CI gate green, exit 0.