The package seats kept incoming, transfers and findings with no read or drop tool (tools review F4). npm_incoming and go_incoming list a seat's open transfers, incoming's fill and the cap its binding carries, and drop one by hand. The same fault as distribution's (running review F2, its second instance): the transfers state's own lifetime dropped a transfer's key and left its chunks, and the drop otherwise waited for the next publish. transfers keeps for good; each open transfer has a key in the new state idle with its own time to live, whose end drops it. The holder counts against its binding's cap. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
377 lines
14 KiB
Go
377 lines
14 KiB
Go
package main
|
|
|
|
// The two package seats' verbs (novox/hq design 51 *The four registry seats*, *One contract*): publish, publish-part,
|
|
// versions and remove on `npm-package-registry` and `go-package-registry`, served as tools named <seat>.<verb> and
|
|
// answered in each seat's own contract types (mesh-contracts npmpackageregistry, gopackageregistry; ADR 0620). The
|
|
// holding itself is internal/packageseats; this file binds it to the forge's package registry, the runtime's bus state
|
|
// and the seats' events.
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/sha1"
|
|
"crypto/sha512"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
goreg "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry"
|
|
npmreg "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry"
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
"git.novox.be/novox/mesh-sdk/go/parts"
|
|
|
|
"gitea/internal/packageseats"
|
|
)
|
|
|
|
// seatStore is the forge's package registry as both seats store into it.
|
|
type seatStore struct{ forge func() (*Forge, error) }
|
|
|
|
func forgeType(seat string) string { return packageseats.Seats[seat] }
|
|
|
|
// forgeVersion is the version as the forge keeps it: a Go module's carries Go's leading v.
|
|
func forgeVersion(seat, version string) string {
|
|
if seat == packageseats.GoSeat && !strings.HasPrefix(version, "v") {
|
|
return "v" + version
|
|
}
|
|
return version
|
|
}
|
|
|
|
func (f *Forge) packagePath(typ, name, version string) string {
|
|
return fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s", url.PathEscape(f.Owner), typ, url.PathEscape(name), url.PathEscape(version))
|
|
}
|
|
|
|
func (s seatStore) Held(seat, name, version string) (string, bool, error) {
|
|
f, err := s.forge()
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
var files []struct {
|
|
SHA256 string `json:"sha256"`
|
|
}
|
|
err = f.GetJSON(context.Background(), f.packagePath(forgeType(seat), name, forgeVersion(seat, version))+"/files", &files)
|
|
switch {
|
|
case errors.Is(err, errNotFound):
|
|
return "", false, nil
|
|
case err != nil:
|
|
return "", false, err
|
|
case len(files) == 0:
|
|
return "", false, fmt.Errorf("%s@%s is listed with no file", name, version)
|
|
}
|
|
return "sha256:" + files[0].SHA256, true, nil
|
|
}
|
|
|
|
func (s seatStore) Put(seat string, p packageseats.Publish, file []byte) (string, error) {
|
|
f, err := s.forge()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
w, err := f.write()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ctx := context.Background()
|
|
if seat == packageseats.GoSeat {
|
|
module, err := goModuleOf(file, forgeVersion(seat, p.Version))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
_, err = w.Send(ctx, http.MethodPut, fmt.Sprintf("/api/packages/%s/go/upload", url.PathEscape(f.Owner)), file, "application/zip", maxBody)
|
|
return module, f.wrote(err)
|
|
}
|
|
doc, err := npmDocument(f.URL, f.Owner, p, file)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
_, err = w.Do(ctx, http.MethodPut, fmt.Sprintf("/api/packages/%s/npm/%s", url.PathEscape(f.Owner), url.PathEscape(p.Name)), doc, maxBody)
|
|
return "", f.wrote(err)
|
|
}
|
|
|
|
func (s seatStore) Versions(seat, name string) ([]packageseats.Version, error) {
|
|
f, err := s.forge()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []packageseats.Version
|
|
for page := 1; page <= 1000; page++ {
|
|
var batch []PackageVersion
|
|
p := fmt.Sprintf("/api/v1/packages/%s?type=%s&q=%s&page=%d&limit=%d", url.PathEscape(f.Owner), forgeType(seat), url.QueryEscape(name), page, pageSize)
|
|
if err := f.GetJSON(context.Background(), p, &batch); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, v := range batch {
|
|
if v.Name != name {
|
|
continue
|
|
}
|
|
d, _, err := s.Held(seat, name, v.Version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, packageseats.Version{Version: strings.TrimPrefix(v.Version, "v"), Digest: d, Published: v.CreatedAt})
|
|
}
|
|
if len(batch) < pageSize {
|
|
break
|
|
}
|
|
}
|
|
// Newest first, by when each was published.
|
|
for i := 1; i < len(out); i++ {
|
|
for j := i; j > 0 && out[j].Published.After(out[j-1].Published); j-- {
|
|
out[j], out[j-1] = out[j-1], out[j]
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s seatStore) Remove(seat, name, version string) error {
|
|
f, err := s.forge()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return f.As(forgeType(seat)).Delete(context.Background(), name, forgeVersion(seat, version))
|
|
}
|
|
|
|
// goModuleOf is the Go module path of a module zip whose every file sits under <module>@<version>/, as Go's module
|
|
// proxy protocol has it; a zip of another version is refused before the forge sees it.
|
|
func goModuleOf(file []byte, version string) (string, error) {
|
|
z, err := zip.NewReader(bytes.NewReader(file), int64(len(file)))
|
|
if err != nil {
|
|
return "", fmt.Errorf("the Go package is not a module zip: %w", err)
|
|
}
|
|
if len(z.File) == 0 {
|
|
return "", errors.New("the Go package's zip holds no file")
|
|
}
|
|
module, rest, at := strings.Cut(z.File[0].Name, "@")
|
|
v, _, ok := strings.Cut(rest, "/")
|
|
if !ok || !at || module == "" {
|
|
return "", fmt.Errorf("the Go package's zip does not hold <module>@<version>/: %s", z.File[0].Name)
|
|
}
|
|
if v != version {
|
|
return "", fmt.Errorf("the Go package's zip is %s, not %s", v, version)
|
|
}
|
|
return module, nil
|
|
}
|
|
|
|
// npmDocument is the body npm's publish sends: the package.json of the tarball's package/, its dist, the tarball as
|
|
// its one attachment, and the dist-tag: the version's tag for a tagged version, so that it never becomes latest.
|
|
func npmDocument(base, owner string, p packageseats.Publish, tgz []byte) (map[string]any, error) {
|
|
manifest, err := npmManifest(tgz)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if manifest["name"] != p.Name || manifest["version"] != p.Version {
|
|
return nil, fmt.Errorf("the tarball is %v@%v, not %s@%s", manifest["name"], manifest["version"], p.Name, p.Version)
|
|
}
|
|
file := path.Base(p.Name) + "-" + p.Version + ".tgz"
|
|
s1, s512 := sha1.Sum(tgz), sha512.Sum512(tgz)
|
|
manifest["_id"] = p.Name + "@" + p.Version
|
|
manifest["dist"] = map[string]any{"shasum": hex.EncodeToString(s1[:]), "integrity": "sha512-" + base64.StdEncoding.EncodeToString(s512[:]),
|
|
"tarball": fmt.Sprintf("%s/api/packages/%s/npm/%s/-/%s/%s", base, owner, p.Name, p.Version, file)}
|
|
tag := "latest"
|
|
if p.Tag != "" {
|
|
tag = p.Tag
|
|
}
|
|
return map[string]any{"_id": p.Name, "name": p.Name, "dist-tags": map[string]string{tag: p.Version},
|
|
"versions": map[string]any{p.Version: manifest},
|
|
"_attachments": map[string]any{file: map[string]any{"content_type": "application/octet-stream", "data": base64.StdEncoding.EncodeToString(tgz), "length": len(tgz)}}}, nil
|
|
}
|
|
|
|
func npmManifest(tgz []byte) (map[string]any, error) {
|
|
gz, err := gzip.NewReader(bytes.NewReader(tgz))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the npm package is not a gzipped tarball: %w", err)
|
|
}
|
|
tr := tar.NewReader(gz)
|
|
for {
|
|
h, err := tr.Next()
|
|
if err == io.EOF {
|
|
return nil, errors.New("the npm package holds no package/package.json")
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the npm package's tarball cannot be read: %w", err)
|
|
}
|
|
if h.Name != "package/package.json" {
|
|
continue
|
|
}
|
|
var m map[string]any
|
|
if err := json.NewDecoder(io.LimitReader(tr, 1<<20)).Decode(&m); err != nil {
|
|
return nil, fmt.Errorf("the npm package's package.json is not JSON: %w", err)
|
|
}
|
|
return m, nil
|
|
}
|
|
}
|
|
|
|
// as is v in T's shape: what crosses the bus is the seat's contract type, read or written through its JSON.
|
|
func as[T any](v any) (T, error) {
|
|
var out T
|
|
raw, err := json.Marshal(v)
|
|
if err == nil {
|
|
err = json.Unmarshal(raw, &out)
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
// outcomeBody is an outcome as the seat's contract's Outcome names it.
|
|
func outcomeBody(o packageseats.Outcome) map[string]any {
|
|
return map[string]any{"name": o.Name, "version": o.Version, "kind": o.Kind, "tag": o.Tag, "digest": o.Digest,
|
|
"fingerprint": o.Fingerprint, "commit": o.Commit, "holder": packageseats.Self, "transfer": o.Transfer,
|
|
"reference": o.Reference, "reason": o.Reason, "module": o.Module}
|
|
}
|
|
|
|
// say says a seat's event; a test hears it instead.
|
|
var say = sayOn
|
|
|
|
// sayOn says an event on the seat's family, in the seat's contract's Outcome.
|
|
func sayOn(seat, event string, o packageseats.Outcome) error {
|
|
var body any
|
|
var err error
|
|
if seat == packageseats.GoSeat {
|
|
body, err = as[goreg.Outcome](outcomeBody(o))
|
|
} else {
|
|
body, err = as[npmreg.Outcome](outcomeBody(o))
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return stdio.SeatPublish("mesh.seat."+seat+".event."+event, body, "")
|
|
}
|
|
|
|
func finding(kind, subject, words string, raised bool) {
|
|
key := health.FindingKey(health.ScopeSeat, packageseats.Self, kind+"."+strings.NewReplacer(".", "_", "@", "_", "/", "_").Replace(subject))
|
|
if !raised {
|
|
_ = stdio.State(health.FindingsState).Delete(key)
|
|
return
|
|
}
|
|
f := health.Finding{V: health.FindingVersion, Key: key, Kind: kind, Ref: key[strings.LastIndex(key, ".")+1:],
|
|
Subject: health.Subject{Scope: health.ScopeSeat, ID: packageseats.Self}, Severity: health.Warning,
|
|
Summary: words, Headline: words, Explanation: words, Resolved: "cleared once it no longer holds",
|
|
Source: kind, Observer: packageseats.Self, At: time.Now().UTC()}
|
|
if _, err := stdio.State(health.FindingsState).Put(key, f); err != nil {
|
|
fmt.Fprintf(os.Stderr, "the finding %s could not be written: %v\n", key, err)
|
|
}
|
|
}
|
|
|
|
// boundCap is incoming's cap from its binding, read once per bundle.
|
|
var boundCap = incomingCap()
|
|
|
|
// busHolder is the holder over the runtime's bus state, the builder's numbering and the forge.
|
|
func busHolder(forge func() (*Forge, error)) (*packageseats.Holder, error) {
|
|
callers, err := packageseats.ConfiguredCallers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &packageseats.Holder{Callers: callers, Objects: parts.BusObjects(packageseats.IncomingObjects),
|
|
Transfers: parts.BusTransfers[packageseats.Opening](packageseats.TransfersState),
|
|
Builder: delivery.BuilderVersions{Ask: stdio.Ask}, Store: seatStore{forge}, Say: say, Finding: finding,
|
|
Cap: boundCap, Idle: armIdle}, nil
|
|
}
|
|
|
|
// SeatTools are both seats' verbs as tools <seat>.<verb>.
|
|
func SeatTools(h func() (*packageseats.Holder, error)) []stdio.Tool {
|
|
return append(verbTools[npmreg.PublishInput, npmreg.PublishAnswer, npmreg.PublishPartInput, npmreg.PublishPartAnswer,
|
|
npmreg.VersionsInput, npmreg.VersionsAnswer, npmreg.RemoveInput, npmreg.RemoveAnswer](packageseats.NpmSeat, h),
|
|
verbTools[goreg.PublishInput, goreg.PublishAnswer, goreg.PublishPartInput, goreg.PublishPartAnswer,
|
|
goreg.VersionsInput, goreg.VersionsAnswer, goreg.RemoveInput, goreg.RemoveAnswer](packageseats.GoSeat, h)...)
|
|
}
|
|
|
|
// chunk is publish-part's input as the holder reads it.
|
|
type chunk struct {
|
|
Data, Part, Transfer string
|
|
Last bool
|
|
Sequence int
|
|
}
|
|
|
|
// verbTools are one seat's four verbs, each reading its input as the seat's contract type and answering in it.
|
|
func verbTools[PI, PA, PPI, PPA, VI, VA, RI, RA any](seat string, holder func() (*packageseats.Holder, error)) []stdio.Tool {
|
|
obj := map[string]any{"type": "object"}
|
|
run := func(do func(h *packageseats.Holder, args map[string]any) (any, error)) func(map[string]any) (any, error) {
|
|
return func(args map[string]any) (any, error) {
|
|
h, err := holder()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return do(h, args)
|
|
}
|
|
}
|
|
return []stdio.Tool{
|
|
{Name: seat + "." + packageseats.VerbPublish, Input: obj,
|
|
Description: "Open a transfer of a package's one file (ADR 0611): its name, the builder's version, kind package, its sha256 digest and the file as its one part; answers the part the forge lacks. A version held with that digest answers held and says published.",
|
|
Run: run(func(h *packageseats.Holder, args map[string]any) (any, error) {
|
|
in, err := as[PI](args)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("publish: %w", err)
|
|
}
|
|
p, err := as[packageseats.Publish](in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
a, err := h.Publish(seat, stdio.CallerOf(args), p)
|
|
out, _ := as[PA](map[string]any{"transfer": a.Transfer, "lacks": a.Lacks, "held": a.Ended && len(a.Lacks) == 0})
|
|
return out, err
|
|
})},
|
|
{Name: seat + "." + packageseats.VerbPublishPart, Input: obj,
|
|
Description: "Send one chunk (at most 760 KiB before base64) of the package's file, from the caller that opened the transfer; the last chunk ends it and the forge says published or refused.",
|
|
Run: run(func(h *packageseats.Holder, args map[string]any) (any, error) {
|
|
in, err := as[PPI](args)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("publish-part: %w", err)
|
|
}
|
|
c, err := as[chunk](in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := base64.StdEncoding.DecodeString(c.Data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("publish-part: data is not base64: %w", err)
|
|
}
|
|
a, err := h.PublishPart(seat, stdio.CallerOf(args), c.Transfer, c.Part, c.Sequence, c.Last, data)
|
|
out, _ := as[PPA](map[string]any{"transfer": c.Transfer, "part": c.Part, "received": c.Sequence + 1, "complete": a.Ended})
|
|
return out, err
|
|
})},
|
|
{Name: seat + "." + packageseats.VerbVersions, Input: obj,
|
|
Description: "The versions the seat holds of one package, newest first, each with its digest.",
|
|
Run: run(func(h *packageseats.Holder, args map[string]any) (any, error) {
|
|
in, err := as[VI](args)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("versions: %w", err)
|
|
}
|
|
q, _ := as[struct{ Name string }](in)
|
|
got, err := h.Versions(seat, stdio.CallerOf(args), q.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
list := []map[string]any{}
|
|
for _, v := range got {
|
|
list = append(list, map[string]any{"name": q.Name, "version": v.Version, "digest": v.Digest, "kind": packageseats.KindPackage})
|
|
}
|
|
return as[VA](map[string]any{"name": q.Name, "versions": list})
|
|
})},
|
|
{Name: seat + "." + packageseats.VerbRemove, Input: obj,
|
|
Description: "Remove a tagged version (the builder, when its branch ended); says removed. An untagged version goes by the forge's retention alone.",
|
|
Run: run(func(h *packageseats.Holder, args map[string]any) (any, error) {
|
|
in, err := as[RI](args)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("remove: %w", err)
|
|
}
|
|
r, _ := as[struct{ Name, Version, Reason string }](in)
|
|
o, err := h.Remove(seat, stdio.CallerOf(args), r.Name, r.Version, r.Reason)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return as[RA](map[string]any{"removed": true, "artifact": map[string]any{"name": o.Name, "version": o.Version, "digest": o.Digest, "kind": o.Kind}})
|
|
})},
|
|
}
|
|
}
|