| 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 |
|
} |