Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/agentbbs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ func main() {
mux.HandleFunc("/verify", a.handleVerify)
mux.HandleFunc("/irc-auth", a.handleIRCAuth) // Ergo auth-script: members-only gate
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) })
// OpenAccess descriptor for the BBS host itself (Caddy proxies the
// well-known path here); the files host serves the same one.
mux.Handle(files.OpenAccessPath, files.OpenAccessHandler())
log.Info("verify endpoint listening", "addr", verifyAddr)
srv := &http.Server{Addr: verifyAddr, Handler: mux, ReadHeaderTimeout: 5 * time.Second}
if err := srv.ListenAndServe(); err != nil {
Expand Down
29 changes: 29 additions & 0 deletions internal/files/openaccess.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"openaccess": "0.1",
"name": "AgentBBS",
"url": "https://bbs.profullstack.com",
"operator": "https://logicsrc.com/.well-known/openprofile.md",
"redirect_uris": [
"https://bbs.profullstack.com/api/v1/openaccess/callback"
],
"jwks": {
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"kid": "Vg7JJd0byYl3",
"x": "bck0ITevVM5tl-uMt-IAzuD_4DBuTpn9zlE0WKimdxw",
"alg": "EdDSA",
"use": "sig"
}
]
},
"scopes": {},
"honours": [
"profullstack.com/all-access"
],
"webhooks": "https://bbs.profullstack.com/api/v1/openaccess/events",
"hubs": [
"https://openaccess.logicsrc.com"
]
}
32 changes: 32 additions & 0 deletions internal/files/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package files

import (
"crypto/rand"
_ "embed"
"encoding/hex"
"errors"
"fmt"
Expand Down Expand Up @@ -71,9 +72,40 @@ func (s *Service) WebHandler(cfg WebConfig) http.Handler {
mux.HandleFunc("/mkdir", h.handleMkdir)
mux.HandleFunc("/delete", h.handleDelete)
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) })
mux.Handle(OpenAccessPath, OpenAccessHandler())
return mux
}

// OpenAccessPath is the well-known location of the OpenAccess descriptor
// (https://logicsrc.com/openaccess). Hubs such as openaccess.logicsrc.com fetch
// it to list the BBS and to link accounts with OAuth 2.1 + PKCE.
const OpenAccessPath = "/.well-known/openaccess.json"

// openAccessDescriptor is the static descriptor served verbatim. It names the
// public signing key (JWKS), the redirect URI and the hubs the BBS trusts.
//
//go:embed openaccess.json
var openAccessDescriptor []byte

// OpenAccessHandler serves the embedded OpenAccess descriptor as JSON with a
// short public cache. It needs no session: hubs fetch it anonymously.
func OpenAccessHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "public, max-age=300")
w.Header().Set("Content-Length", strconv.Itoa(len(openAccessDescriptor)))
if r.Method == http.MethodHead {
return
}
_, _ = w.Write(openAccessDescriptor)
})
}

// --- session helpers --------------------------------------------------------

func (h *webSrv) lookup(r *http.Request) (string, bool) {
Expand Down
31 changes: 31 additions & 0 deletions internal/files/web_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package files

import (
"bytes"
"encoding/json"
"io"
"mime"
"mime/multipart"
Expand Down Expand Up @@ -394,3 +395,33 @@ func TestWebPublicReadOnlyByDefault(t *testing.T) {
t.Fatalf("public upload: want redirect, got %d", rr.Code)
}
}

func TestWebOpenAccessDescriptor(t *testing.T) {
h, _ := webTestHandler(t)
rr := httptest.NewRecorder()
h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, OpenAccessPath, nil))
if rr.Code != http.StatusOK {
t.Fatalf("status: want 200, got %d", rr.Code)
}
if ct := rr.Header().Get("Content-Type"); ct != "application/json" {
t.Fatalf("content-type: got %q", ct)
}
if cc := rr.Header().Get("Cache-Control"); cc != "public, max-age=300" {
t.Fatalf("cache-control: got %q", cc)
}
var doc struct {
OpenAccess string `json:"openaccess"`
URL string `json:"url"`
}
if err := json.Unmarshal(rr.Body.Bytes(), &doc); err != nil {
t.Fatalf("descriptor is not JSON: %v", err)
}
if doc.OpenAccess == "" || doc.URL == "" {
t.Fatalf("descriptor missing openaccess/url: %s", rr.Body.String())
}
rr = httptest.NewRecorder()
h.ServeHTTP(rr, httptest.NewRequest(http.MethodPost, OpenAccessPath, nil))
if rr.Code != http.StatusMethodNotAllowed {
t.Fatalf("POST: want 405, got %d", rr.Code)
}
}
6 changes: 6 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,12 @@ ${DOMAIN} {
reverse_proxy http://${HTTP_ADDR}
}

# OpenAccess descriptor (https://logicsrc.com/openaccess): lets hubs list
# the BBS and link accounts with OAuth 2.1 + PKCE. Static JSON, no auth.
handle /.well-known/openaccess.json {
reverse_proxy http://${HTTP_ADDR}
}

# IRC over WebSocket: Caddy terminates TLS and proxies to Ergo's loopback
# WebSocket listener, so web clients hit wss://${DOMAIN}/irc and agents get a
# WebSocket transport without exposing another public port. (No-op if IRC=0;
Expand Down