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 . 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" builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "io" "net/http" "net/url" "os" "path" "strings" "time" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" goreg "git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry" npmreg "git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry" "git.novox.be/novox/mesh-delivery/sdk/parts" stdio "git.novox.be/novox/mesh-sdk/go" "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 @/, 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 @/: %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 := meshconditionsclient.Key(meshconditionsclient.ScopeSeat, packageseats.Self, kind+"."+strings.NewReplacer(".", "_", "@", "_", "/", "_").Replace(subject)) if !raised { _ = stdio.State(meshconditions.StateFindings).Delete(key) return } f := meshconditions.FindingsByKeyValue{V: meshconditionsclient.FindingVersion, Key: key, Kind: kind, Ref: key[strings.LastIndex(key, ".")+1:], Subject: meshconditions.Subject{Scope: meshconditionsclient.ScopeSeat, ID: packageseats.Self}, Severity: meshconditionsclient.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(meshconditions.StateFindings).Put(key, f); err != nil { fmt.Fprintf(os.Stderr, "the finding %s could not be written: %v\n", key, err) } } // recordAct records a hand act through the seat mesh-hand-acts alone. func recordAct(q meshhandactsclient.Request) error { _, err := meshhandactsclient.Recorder{Ask: stdio.Ask}.Record(q) return err } // boundCap is incoming's cap from its binding, read once per bundle. var boundCap = parts.BusCap(packageseats.IncomingObjects) // 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: builds.BuilderVersions{Ask: stdio.Ask}, Store: seatStore{forge}, Say: say, Finding: finding, Cap: boundCap, Record: recordAct, Idle: parts.BusArm(packageseats.IdleState)}, nil } // SeatTools are both seats' verbs as tools .. 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}}) })}, } }