mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 09:14:16 +00:00
appview: resolve managed hold names off the privacy render path
Follow-up to 6e77311. Listing the operated holds on /privacy put
resolveHoldDisplayName in the render path, where it makes up to two sequential
network calls per DID: the DID document fetch, then handle verification. /privacy
is public and unauthenticated, and the server sets no HTTP read or write timeout,
so an unreachable hold or a plc.directory outage stalled the page for every
visitor. The identity directory's negative-cache TTL is short, so the stall
recurred rather than settling after the first hit.
The DIDs come from config and never change while the process runs, so resolution
happens once in a background goroutine and lands in an atomic.Pointer. The
handler is constructed once at route registration, so the cache is process-wide.
Until resolution completes the page renders offline names, which are already
correct for did:web holds since those decode straight from the DID.
Dropped the did:plc truncation. resolveHoldDisplayName's last fallback cut a DID
to 24 characters plus an ellipsis, which is shorter than a did:plc, so the result
could not be resolved back to a hold. That is tolerable in a settings dropdown
and actively misleading in a privacy policy naming the services we operate.
Shortening for display belongs in the template. The non-network fallbacks are now
in holdDisplayNameOffline so the background resolver and the render path share
them.
Also fixed the surrounding copy, which scoped coverage to *.<site> domains while
the list immediately above it could contain holds on other domains — the two
sentences contradicted each other. It now refers to the listed holds, and only
makes that claim when there is a real list; with no managed holds configured the
template still shows an illustrative placeholder, which must not be presented as
fact in a legal document.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
500ee2f8d1
commit
3298797603
@@ -3,9 +3,15 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// managedHoldResolveTimeout bounds the one-shot background resolution of the
|
||||
// managed-hold display names.
|
||||
const managedHoldResolveTimeout = 30 * time.Second
|
||||
|
||||
// LegalPageData contains data for legal pages (terms, privacy).
|
||||
type LegalPageData struct {
|
||||
PageData
|
||||
@@ -52,19 +58,54 @@ func formatLegalDate(raw string) string {
|
||||
// PrivacyPolicyHandler handles the /privacy page
|
||||
type PrivacyPolicyHandler struct {
|
||||
BaseUIHandler
|
||||
|
||||
// holdNames caches the managed-hold display names; see managedHoldNames.
|
||||
// The handler is constructed once at route registration and shared across
|
||||
// requests, so this cache is process-wide.
|
||||
holdNamesOnce sync.Once
|
||||
holdNames atomic.Pointer[[]string]
|
||||
}
|
||||
|
||||
// resolveManagedHoldNames maps the configured managed-hold DIDs to friendly
|
||||
// display names (handle, decoded did:web domain, or truncated did:plc) for
|
||||
// listing on the privacy page.
|
||||
func (h *PrivacyPolicyHandler) resolveManagedHoldNames(ctx context.Context) []string {
|
||||
names := make([]string, 0, len(h.ManagedHolds))
|
||||
for _, did := range h.ManagedHolds {
|
||||
if name := resolveHoldDisplayName(ctx, &h.BaseUIHandler, did); name != "" {
|
||||
names = append(names, name)
|
||||
// managedHoldNames returns display names for the configured managed holds,
|
||||
// resolved once and cached.
|
||||
//
|
||||
// Resolution is deliberately off the render path. resolveHoldDisplayName makes
|
||||
// up to two sequential network calls per DID (DID document, then handle
|
||||
// verification), /privacy is public and unauthenticated, and the server sets no
|
||||
// HTTP write timeout — so resolving inline let an unreachable hold or a
|
||||
// plc.directory outage stall the page for tens of seconds for every visitor,
|
||||
// recurring each time the identity directory's short negative-cache entry
|
||||
// lapsed. The DIDs come from config and never change while the process runs, so
|
||||
// one background pass is enough; until it lands the page renders the offline
|
||||
// names, which are already correct for did:web holds.
|
||||
func (h *PrivacyPolicyHandler) managedHoldNames() []string {
|
||||
h.holdNamesOnce.Do(func() {
|
||||
offline := make([]string, 0, len(h.ManagedHolds))
|
||||
for _, did := range h.ManagedHolds {
|
||||
if name := holdDisplayNameOffline(did); name != "" {
|
||||
offline = append(offline, name)
|
||||
}
|
||||
}
|
||||
h.holdNames.Store(&offline)
|
||||
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), managedHoldResolveTimeout)
|
||||
defer cancel()
|
||||
|
||||
names := make([]string, 0, len(h.ManagedHolds))
|
||||
for _, did := range h.ManagedHolds {
|
||||
if name := resolveHoldDisplayName(ctx, &h.BaseUIHandler, did); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
h.holdNames.Store(&names)
|
||||
}()
|
||||
})
|
||||
|
||||
if names := h.holdNames.Load(); names != nil {
|
||||
return *names
|
||||
}
|
||||
return names
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *PrivacyPolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -81,7 +122,7 @@ func (h *PrivacyPolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
CompanyName: company,
|
||||
Jurisdiction: jurisdiction,
|
||||
LastUpdated: formatLegalDate(privacyLastUpdated),
|
||||
ManagedHolds: h.resolveManagedHoldNames(r.Context()),
|
||||
ManagedHolds: h.managedHoldNames(),
|
||||
}
|
||||
|
||||
if err := h.Templates.ExecuteTemplate(w, "privacy", data); err != nil {
|
||||
|
||||
@@ -347,8 +347,32 @@ func (h *SettingsHandler) buildSubscriptionDisplay(userDID string) SubscriptionD
|
||||
return display
|
||||
}
|
||||
|
||||
// holdDisplayNameOffline derives a display name for a hold DID without making
|
||||
// any network call: the decoded domain for did:web, the DID itself otherwise.
|
||||
// Returns "" for an empty DID.
|
||||
//
|
||||
// did:plc values are returned whole. This used to truncate them to 24 chars plus
|
||||
// an ellipsis, which is shorter than a did:plc and therefore yields a string
|
||||
// that cannot be resolved back to a hold — actively misleading anywhere the name
|
||||
// stands in for the identity, such as the privacy page's list of operated
|
||||
// services. Shortening for display is the template's job.
|
||||
func holdDisplayNameOffline(did string) string {
|
||||
if did == "" {
|
||||
return ""
|
||||
}
|
||||
if after, ok := strings.CutPrefix(did, "did:web:"); ok {
|
||||
if decoded, err := url.QueryUnescape(after); err == nil {
|
||||
return decoded
|
||||
}
|
||||
return after
|
||||
}
|
||||
return did
|
||||
}
|
||||
|
||||
// resolveHoldDisplayName resolves a hold DID to a human-readable handle via the
|
||||
// identity directory. Falls back to domain extraction (did:web) or truncation (did:plc).
|
||||
// identity directory, falling back to [holdDisplayNameOffline]. This makes up to
|
||||
// two sequential network calls, so callers on a request path should cache the
|
||||
// result rather than resolving per render.
|
||||
func resolveHoldDisplayName(ctx context.Context, h *BaseUIHandler, did string) string {
|
||||
if did == "" {
|
||||
return ""
|
||||
@@ -365,20 +389,7 @@ func resolveHoldDisplayName(ctx context.Context, h *BaseUIHandler, did string) s
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: extract domain from did:web
|
||||
if after, ok := strings.CutPrefix(did, "did:web:"); ok {
|
||||
domain := after
|
||||
if decoded, err := url.QueryUnescape(domain); err == nil {
|
||||
return decoded
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
// Fallback: truncate did:plc
|
||||
if len(did) > 24 {
|
||||
return did[:24] + "..."
|
||||
}
|
||||
return did
|
||||
return holdDisplayNameOffline(did)
|
||||
}
|
||||
|
||||
// UpdateDefaultHoldHandler handles updating the default hold
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<li>Layer records in the hold's embedded PDS linking your DID to blob references</li>
|
||||
<li>Crew membership records for access control</li>
|
||||
</ul>
|
||||
<p class="mt-2">Hold services on <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">*.{{ .SiteURL }}</code> domains are operated by us and covered by this policy.</p>
|
||||
{{ if .ManagedHolds }}<p class="mt-2">The hold services listed above are the ones we operate, and they are the ones covered by this policy. Any other hold, including one you deploy yourself, is not.</p>{{ else }}<p class="mt-2">Hold services we operate are covered by this policy. Holds deployed by anyone else, including any you deploy yourself, are not.</p>{{ end }}
|
||||
|
||||
<h3 class="text-lg font-medium mt-6">User-Deployed Hold Services (BYOS)</h3>
|
||||
<p>You may use "Bring Your Own Storage" by deploying your own hold service. Data on user-deployed holds is governed by that operator's privacy policy, not ours. We can request deletion on your behalf but cannot guarantee it for services we do not control.</p>
|
||||
|
||||
Reference in New Issue
Block a user