# 2026-07-17: join URLs + cert pinning (remote-issuance step 1) Implemented step 1 of [/plans/remote-token-issuance.md](/plans/remote-token-issuance.md) on branch `join-string`: the no-sidecar prefix (join-string grammar + TOFU cert pinning), after the Option C sidecar decision landed in the plan (v3) and roadmap (v58). ## What shipped - `client/joinurl` (new package): Build/Parse of `mark://host#token=...&fp=sha256:` join URLs, cert fingerprint helpers (DER, PEM), `VerifyPin` TLS verifier. Fragment carries the credentials; it never appears in any protocol request. - `client/fetch`: `Options.Pins` (host to fingerprint map). A pinned host gets `InsecureSkipVerify` plus `VerifyPin` at dial time, per host, so a multi-host crawler client pins only what it should. Wire tests: right pin, wrong pin (handshake failure with re-join hint), bare-hostname fallback, unpinned hosts still CA-verify. - `client/internal/tokens`: entry gains `pin` field (tokens.toml, additive), `SetPin`/`Pins`/`ResolvePin` (flag > `DEMARKUS_PIN` > stored). - All three clients wire stored pins; `demarkus-mcp` gains `-pin`; new `demarkus join ''` stores token + pin in one paste. - `demarkus-token join` subcommand: composes with generate via pipe, `-cert` embeds the fingerprint. - `install.sh`: summary now prints a ready-to-paste `/soul-join ` and `demarkus join` line. - Plugin: `registry soul-join` accepts a join URL (fragment supplies token + pin), souls catalog gains a 5th pin column (all readers were length-guarded), `mcp-serve` passes `-pin` instead of `-insecure` for pinned souls. `commands/soul-join.md` updated; plugin 0.12.16 to 0.13.0. - Docs: `docs/site/tools/index.md` join-URL section. ## Decisions and gotchas - **Never pin Let's Encrypt certs**: leaves rotate on renewal (install.sh runs certbot twice daily) and would break every pinned client. install.sh embeds `fp=` only for custom certs; LE domains pass CA verification anyway. The dev self-signed cert has no stable file, so that path falls back to `--insecure` with a note. - **Go url fragment gotcha**: `u.Fragment` is pre-decoded; parsing it with `ParseQuery` corrupts `+` and `%` in tokens. Use `u.EscapedFragment()`. Conversely `url.Values.Encode` escapes `:` to `%3A`, uglifying the paste string; the fragment is hand-built (pin literal, token QueryEscaped). - Pin resolution mirrors token resolution exactly (explicit > env > stored), per the shared-behavior guideline; pins ride tokens.toml instead of a new file. - mcp-serve precedence: pin wins over insecure (pin verifies exactly one cert; insecure verifies nothing). ## Verified live Self-signed server on :6399: `demarkus-token generate | join -cert` produced the URL, `demarkus join` stored both credentials, publish + fetch ran with no `--insecure`, and a corrupted pin failed at the QUIC handshake with the actionable mismatch error. Plugin path: `registry soul-join ` wrote the 0600 token file, `pinned=1`, catalog row carries the pin. Full `make test` + `bash pre-commit.sh` clean. Branch ready for PR; binary pin bumps ride the release automation after merge.