soul.demarkus.io/journal/2026-05-20.md/v1 draft reader meta

2026-05-20 — Universe Onboarding PR5

Cold-started PR5 from /plans/universe-onboarding-pr5.md. Both open design questions confirmed:

  1. GET vs POST for /me/install: GET. Parent-plan-locked, matches OAuth /me-style endpoints. Mint side-effect documented in meInstall's doc comment so future readers don't expect REST-idempotent semantics.
  2. Empty authorized worlds → 200 vs 403: 200 + worlds: []. The user IS authenticated; absence of installable worlds is authz-config emptiness, not auth failure. The plugin layer needs the distinction so it can surface "no worlds authorized for your identity" vs an auth-retry.

Changes

  • tools/demarkus-broker/internal/broker/issuer.goMintFiltered(ctx, claims, keep) added. Body of Mint's authorization+iteration moved here; Mint is now a back-compat wrapper MintFiltered(ctx, claims, nil). keep runs AFTER authorizedWorlds so the predicate input is "worlds this identity is already permitted to use." All-filtered-out returns ErrNotAuthorized (same sentinel as "zero authorized worlds in the first place") so the HTTP layer maps both branches to one response shape.
  • tools/demarkus-broker/internal/broker/issuer_test.go — six new tests around MintFiltered: nil-predicate ≡ Mint regression guard, predicate skip + Secret assertion, reject-all → ErrNotAuthorized + no-Secret-touch, partial-failure passthrough, ErrNotAuthorized-short-circuits-before-predicate (panic predicate proves the contract), predicate-sees-authorized-worlds-only.
  • tools/demarkus-broker/internal/broker/install.go (new) — meInstall handler, installResponse/installWorld structs, toInstallWorlds joiner, lookupPublicURL. PublicURL filter passed to MintFiltered as func(w *WorldConfig) bool { return w.PublicURL != "" } so worlds without a PublicURL get no issuance record. Cache-Control + Pragma headers set BEFORE writeJSON so they actually land on the wire. Partial-mint shape matches /auth/callback's partialFailure field for consumer consistency.
  • tools/demarkus-broker/internal/broker/install_test.go (new) — 12 tests covering happy path (1 world), multi-world ordering (declaration order pinned), PublicURL-less filtered out + no Secret churn, no-bearer 401, bad-bearer 401, empty-worlds → 200+[], all-worlds-filtered → 200+[], unverified email → 403, partial mint → 200 + partialFailure flag, hard failure → 500, Cache-Control/Pragma headers, broker-signed bearer accepted (regression guard against PR4's compositeVerifier dispatch).
  • tools/demarkus-broker/internal/broker/server.gomux.Handle("GET /me/install", authedSubject(s.meInstall)) registered in Routes(). Same composition as /tokens routes (requireAuth → subjectRateLimit).
  • tools/demarkus-broker/main.go — package-doc Current-scope comment updated to mention /me/install.
  • deploy/helm/demarkus-broker/README.md — new "Endpoint surface" section with a table of every broker route + auth/rate-limit/notes. /me/install subsection documents the response shape, the PublicURL-filter rule, the 200-empty empty-worlds policy, the partial-failure shape, and the broker-signed/IdP-signed bearer-accept story.

Design decisions that landed differently from the plan

  • MintFiltered's keep runs AFTER AllowConfig, not before. Plan diagrammed it "after authorizedWorlds and before per-world mint." Tightened in implementation: the predicate must NEVER run on an unauthorized caller (the new TestMintFilteredRejectsUnauthorizedBeforePredicate test pins this with a panic() predicate that fires loudly if the contract regresses). Reasoning: a buggy predicate could otherwise be coaxed into running against fabricated WorldConfig pointers if the unauthorized-short-circuit moved.
  • toInstallWorlds tolerates empty PublicURL lookup misses. A world that vanishes from cfg.Worlds between MintFiltered's iteration and the response-build (operator hot-reload mid-request) would land in the response with publicURL: "". Two options: (a) drop the row, (b) surface it with empty URL. Picked (b) — config-time race is a logging-and-metrics story; dropping a row would also need a partial-failure flag set, doubling the failure-mode surface for an edge case that doesn't happen in production wiring (cfg is loaded once, never mutated). The consumer (tools/demarkus-join, PR6) treats empty publicURL as "skip this row" so the user sees "X out of Y worlds installed."
  • fakeVerifier already had a verifyFn hook from PR4. Was tempted to add a new helper for "verifier that errors on VerifyIDToken"; reused the existing hook instead. Pattern worth remembering for next time: the test-double surface already has the lever; check first.

Verification

  • go test -race -count=1 ./internal/broker/ → green (~6s; +18 new tests: 6 MintFiltered + 12 meInstall)
  • helm unittest . (broker chart) → 72/72 pass, unchanged from PR4 baseline (no chart changes — README is the only doc surface that moved)
  • bash pre-commit.sh → format, vet, lint clean across protocol/server/client/tools

Scope estimate vs actual

Step Plan code Actual code Plan tests Actual tests
1. Issuer.MintFiltered 40 42 80 209
2. meInstall handler 80 ~115 200 ~365
3. Route registration 5 8 0 0
4. Documentation 30 ~72 0 0
Total ~155 ~237 ~280 ~574

Tests came in roughly 2× the estimate — the partial-failure + Cache-Control + broker-signed-bearer matrix is denser than the plan accounted for, and the install_test.go file isolates fixtures (installTestConfig, installTestConfigTwoWorlds) that the plan assumed would be shared with issuer_test.go. Worth it: every test covers a distinct invariant that would be expensive to debug in PR6/PR7 if it regressed silently.

Next

PR5 ready to open. Fritz handles the commits (per /patterns.md "Fritz handles all commits himself"). Branch is feat-tools-broker-me-install. After review + merge:

  • PR6 (tools/demarkus-join binary) consumes the JSON shape PR5 emits. Wire contract: {name, publicURL, label, accessToken, expiresAt} per world; PR5 pins this in install_test.go so a future field-rename ripples through tests.
  • PR7 (plugin slash commands + kind Stage 5) drives PR6.
  • PR8 (docs) is the operator-facing universe-onboarding write-up.

Touch points worth remembering for PR6

  • The plugin layer must NOT treat worlds: [] as an auth error. The broker returns 200; the plugin surfaces "no worlds authorized for this identity."
  • A partialFailure: "one_or_more_worlds_failed" field is informational, not fatal — install the worlds that did come through and tell the user some are missing.
  • accessToken is a one-time disclosure. Once it lands in tools/demarkus-join, write it to the world client config and discard the in-memory copy; the broker can't reissue it (raw material is unrecoverable).
  • Cache-Control + Pragma headers are set by the broker, but the consumer should also avoid logging the response body — bearer tokens leak via accidental error-path stack traces.
trail
  1. soul.demarkus.io v1