coverage~bigbes/sr-ht-ecore3bd158fbapimeta/apimeta.go

Coverage
87.5% 7/8 statements
Δ
Blob
27a9a9b
Uncovered L73-L74
1 // Package apimeta serves the api-meta.json a SourceHut service publishes beside
2 // its GraphQL endpoint, for the services on this instance that mount /query
3 // themselves.
4 //
5 // core-go serves this file for a service assembled through
6 // server.Server.WithSchema, which mounts /query on the *authenticated* router.
7 // A service whose API must answer anonymous callers — a public repository, a
8 // public database — cannot use that: core-go's auth middleware 401s an
9 // un-cookied request. Such a service mounts its own /query, and then owes the
10 // instance this file too, because nothing else will serve it.
11 //
12 // # Why the scope list must never be null
13 //
14 // meta.sr.ht fetches api-meta.json from every service it discovers when it
15 // renders /oauth2/personal-token, and iterates each service's "scopes" to build
16 // the grant checkboxes. A JSON `null` there is not a service with no scopes: it
17 // is a nil iteration in meta, which is a 500 on the personal-token page for the
18 // WHOLE instance — every service's grants, not just the one that answered
19 // badly. That is why Handler takes its scopes variadically and marshals an
20 // empty slice for none: the failure mode is one nobody would find by testing
21 // the service that caused it.
22 //
23 // # What a scope is
24 //
25 // The part after the service name in a personal-token grant. A service that
26 // checks "dolt.sr.ht/repos:RO" publishes "repos" here; meta prefixes the
27 // service name itself. The two spellings are the same fact written twice, so a
28 // service should assert them equal in a test rather than hope.
29 package apimeta
30
31 import (
32 "encoding/base64"
33 "encoding/json"
34 "net/http"
35
36 "sourcecraft.dev/bigbes/sr-ht-core/crypto"
37 )
38
39 // Path is where meta.sr.ht and every other client look for this file. It is
40 // core-go's own path, so a service that mounts /query itself stays
41 // indistinguishable from one that did not.
42 const Path = "/query/api-meta.json"
43
44 // Meta is the document itself. It is exported so a test can unmarshal into it
45 // rather than into a map with the field names spelled a second time.
46 type Meta struct {
47 // Scopes are the grants this service defines, without the service prefix.
48 // It marshals as [] and never as null — see the package comment.
49 Scopes []string `json:"scopes"`
50 // WebhookPubkey is the instance's Ed25519 webhook public key, base64. It is
51 // the same key for every service (it comes from the shared [webhooks]
52 // private-key), and it is what a webhook consumer verifies payloads with.
53 WebhookPubkey string `json:"webhook-pubkey"`
54 }
55
56 // Handler serves api-meta.json for a service declaring these scopes.
57 //
58 // crypto.InitCrypto must have run — core-go's server.New does it — or the
59 // published key is the empty string. That is a boot-order bug rather than a
60 // runtime condition, so it is not reported per request.
61 4 func Handler(scopes ...string) http.HandlerFunc {
62 4 // Marshalled once: the document cannot change between requests, and
63 4 // building it per request would be one more thing that can fail on a path
64 4 // meta.sr.ht calls for the whole instance.
65 4 if scopes == nil {
66 1 scopes = []string{}
67 1 }
68 4 body, err := json.Marshal(Meta{
69 4 Scopes: scopes,
70 4 WebhookPubkey: base64.StdEncoding.EncodeToString(crypto.WebhookPubkey),
71 4 })
72 4 if err != nil {
73 0 // Two strings and a string slice; there is no input that reaches this.
74 0 panic("apimeta: marshalling api-meta.json: " + err.Error())
75 }
76
77 6 return func(w http.ResponseWriter, r *http.Request) {
78 6 w.Header().Set("Content-Type", "application/json")
79 6 _, _ = w.Write(body)
80 6 }
81 }