~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

Private repositories have sites, read by their members alone

Merged
Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/private-sites

What changes

Deploying. knotless repo site now deploys a private repository's site. Its record and blobs are kept in the repository's space, as its packs are, and the command then shows the appview the space, as any write there does. It prints the address, https://<repo key>-<plc id>.private.knotless.page/, a host of its own named by the repository's key, so a rename does not move it. An owner with no did:plc is refused. The dev.knotless.space type lists dev.knotless.site among its collections.

Serving. serve indexes dev.knotless.site records from spaces it syncs. A request to a private site's host with no pass is sent to knotless.dev's new GET /oauth/site. There a visitor who is not logged in is sent to /?login=<path>, where the login dialog opens and comes back to that path. A logged-in visitor is proven a member, with a credential the owner's PDS gives only to members, and is sent back with a ticket. The ticket is signed, for that host alone, lasts a minute and is taken once. The host trades it at /.knotless/enter for a __Host-knotless_site cookie that lasts eight hours. Each request is checked against the pass, which a logout ends, and against membership, asked again after REPROVE as the pages ask it. Answers are private, no-store. A private site's files are kept apart from every other site's, so no record elsewhere naming the same hash is served them.

Deploy. Caddy also answers *.private.knotless.page, with a wildcard certificate from the same DNS challenge. The zone's * record already covers it.

Docs: design/pages.md (Private sites, now as built, with a sequence diagram), guide/sites.md, design/serve.md, reference/api.md, reference/cli.md, deploy/README.md.

Why

Private sites were the part of pages left undone. This follows the design pages.md already set out: membership is the gate, and each site gets its own origin, so no script on the owner's public site can fetch a private one with the visitor's cookie.

How it was checked

  • private_sites_are_read_by_members in crates/knotless-serve/tests/pages.rs, over the spaces alpha:
    • deploy into the space, at its own host
    • sent for a ticket, and to log in when logged in as no one
    • a ticket refused on another host, and taken once
    • the site served with the pass, not on another host
    • a stranger given no ticket
    • a member unshared reads it no more, and reads it again when shared again
    • a redeploy read once the space syncs
    • a pass ended by logging out
    • the site gone once taken down
  • Unit tests for the host label both ways, and for indexing a space's site record.
  • clippy on both crates, and the unit tests, run here. test, web, lexicons, docs and shots are left to CI.

Shots-moved: none

Shortcomings

  • setup.sh has to be run again on the prod host for Caddy to take *.private.knotless.page. Until then private sites' hosts get no certificate.
  • private.knotless.page is not on the Public Suffix List yet, as knotless.page is not. Until it is, one private site's pages can set a cookie for the whole of private.knotless.page, which others then receive. No pass can be set this way, since each is __Host-.
  • Serve finds a private site's repository only in a space it already keeps an index of. That is true once anyone deploys it (the deploy shows the appview the space), and for any owner who has logged in to knotless.dev.
  • A redeploy is seen as soon as following the space hears of it, as with the space's other records.
  • The login dialog opened from ?login= is the existing dialog, so no page moves. The shots check does not draw the opened state.

🤖 Generated with Claude Code

Approved

This pull lets knotless repo site deploy a private repository's site into its space. serve then answers that site at <key>-<plc id>.private.<domain> to proven members only. I read the serve, login, index, client, space and setup changes and found nothing that must change.

  • Host and ticket handling: The private host check runs before the public owner lookup. private_site accepts only strict base32 labels of fixed length, so the label cannot carry path characters into kept_dir/private/<label>.
  • Redirect targets: /oauth/site honours only hosts that parse as private hosts under this domain. enter runs to through back_to, so neither endpoint is an open redirect.
  • Ticket and pass binding: The ticket is signed for one host, lasts a minute and is taken once. Both ticket and pass go through login::holds, so a logout kills them.
  • Index lookup: Record lookup is keyed by the full space URI plus the owner's DID. A space member cannot make the host serve a record they wrote under their own DID.
  • File cache: Each site's files are kept in a per-label directory, so another site's record naming the same hash is not served them.

I did not run it. The CI checks (test, web, lexicons, docs, shots) are for you to confirm, as the body says.

One minor point, not a blocker: the ticket travels in a URL query. It is single-use and lasts 60 seconds, so Caddy or access logs recording it are harmless in practice.

Approved

This round adds a merge of main and one commit on top of the approved head. The commit updates two tests for the new feature: the SPELLED_OUT scope assertion now expects dev.knotless.site in the space collections, and the CLI sites test now checks that a private repository's site deploys and prints a .private.knotless.page/ address. The old assertion expected a refusal, so it had to change. Nothing in the serve, login or index paths changed since my last review, and I found no new problems. I did not run the tests. CI covers them.

Approved

The only change since my last review is a small refactor in index.rs. The inline row closure in the private repository lookup becomes a named uri_and_name function, which returns the same two columns. Behaviour is unchanged, and I found nothing in the serve, login or index paths to flag. I did not run the tests. CI covers them.

Merged as 444918b