mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-10-01 19:43:49 +00:00
Serve the OpenAccess descriptor at /.well-known/openaccess.json (#127)
Spec: https://logicsrc.com/openaccess Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SWRffW4ifQPUrGXJtgYWMd
This commit is contained in:
parent
d2e4b56ae7
commit
aad61eec17
5 changed files with 101 additions and 0 deletions
|
|
@ -272,6 +272,9 @@ func main() {
|
||||||
mux.HandleFunc("/verify", a.handleVerify)
|
mux.HandleFunc("/verify", a.handleVerify)
|
||||||
mux.HandleFunc("/irc-auth", a.handleIRCAuth) // Ergo auth-script: members-only gate
|
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")) })
|
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)
|
log.Info("verify endpoint listening", "addr", verifyAddr)
|
||||||
srv := &http.Server{Addr: verifyAddr, Handler: mux, ReadHeaderTimeout: 5 * time.Second}
|
srv := &http.Server{Addr: verifyAddr, Handler: mux, ReadHeaderTimeout: 5 * time.Second}
|
||||||
if err := srv.ListenAndServe(); err != nil {
|
if err := srv.ListenAndServe(); err != nil {
|
||||||
|
|
|
||||||
29
internal/files/openaccess.json
Normal file
29
internal/files/openaccess.json
Normal file
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
_ "embed"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -71,9 +72,40 @@ func (s *Service) WebHandler(cfg WebConfig) http.Handler {
|
||||||
mux.HandleFunc("/mkdir", h.handleMkdir)
|
mux.HandleFunc("/mkdir", h.handleMkdir)
|
||||||
mux.HandleFunc("/delete", h.handleDelete)
|
mux.HandleFunc("/delete", h.handleDelete)
|
||||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) })
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) })
|
||||||
|
mux.Handle(OpenAccessPath, OpenAccessHandler())
|
||||||
return mux
|
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 --------------------------------------------------------
|
// --- session helpers --------------------------------------------------------
|
||||||
|
|
||||||
func (h *webSrv) lookup(r *http.Request) (string, bool) {
|
func (h *webSrv) lookup(r *http.Request) (string, bool) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
|
@ -394,3 +395,33 @@ func TestWebPublicReadOnlyByDefault(t *testing.T) {
|
||||||
t.Fatalf("public upload: want redirect, got %d", rr.Code)
|
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
setup.sh
6
setup.sh
|
|
@ -774,6 +774,12 @@ ${DOMAIN} {
|
||||||
reverse_proxy http://${HTTP_ADDR}
|
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
|
# 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 listener, so web clients hit wss://${DOMAIN}/irc and agents get a
|
||||||
# WebSocket transport without exposing another public port. (No-op if IRC=0;
|
# WebSocket transport without exposing another public port. (No-op if IRC=0;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue