package main import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) // The forge as this bundle reaches it: its own HTTP interface on the machine, as the forge's admin // account, which the vault delivered into a file this module owns (novox/hq ADR 0086). Basic // authentication, so this bundle mints nothing and keeps nothing: the TypeScript bundle's token is its // own. The password is read when a call is made and never logged, answered or put in an error. // Forge is the forge's HTTP interface. type Forge struct { URL string // e.g. http://127.0.0.1:3000 User string PasswordFile string Owner string // the package registry's owner, as the seat serves it HTTP *http.Client } // ForgeFromEnv is the forge as the manifest hands it to this bundle. func ForgeFromEnv() (*Forge, error) { f := &Forge{ URL: strings.TrimRight(os.Getenv("MESH_GITEA_URL"), "/"), User: os.Getenv("MESH_GITEA_ADMIN_USER"), PasswordFile: os.Getenv("MESH_GITEA_ADMIN_PASSWORD_FILE"), Owner: os.Getenv("MESH_NPM_OWNER"), } var missing []string for name, v := range map[string]string{"MESH_GITEA_URL": f.URL, "MESH_GITEA_ADMIN_USER": f.User, "MESH_GITEA_ADMIN_PASSWORD_FILE": f.PasswordFile, "MESH_NPM_OWNER": f.Owner} { if v == "" { missing = append(missing, name) } } if len(missing) > 0 { return nil, fmt.Errorf("the forge cannot be reached: %s not set by the manifest", strings.Join(missing, ", ")) } return f, nil } // errNotFound is the forge answering 404. var errNotFound = errors.New("not found") // statusError is an answer the forge gave that is not a success, said without the request's credential. type statusError struct { method, path string status int said string } func (e *statusError) Error() string { return fmt.Sprintf("the forge answered %d to %s %s: %s", e.status, e.method, e.path, e.said) } func (f *Forge) client() *http.Client { if f.HTTP != nil { return f.HTTP } return &http.Client{Timeout: 20 * time.Second} } func (f *Forge) password() (string, error) { raw, err := os.ReadFile(f.PasswordFile) if err != nil { // The path is the manifest's, not a secret; the content never appears. return "", fmt.Errorf("the forge's admin password cannot be read from its file: %w", err) } return strings.TrimSpace(string(raw)), nil } // do sends one request and answers the body; 404 is errNotFound. func (f *Forge) do(ctx context.Context, method, path string) ([]byte, error) { password, err := f.password() if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, method, f.URL+path, nil) if err != nil { return nil, err } req.SetBasicAuth(f.User, password) req.Header.Set("Accept", "application/json") res, err := f.client().Do(req) if err != nil { return nil, fmt.Errorf("the forge did not answer %s %s: %w", method, path, err) } defer res.Body.Close() body, err := io.ReadAll(io.LimitReader(res.Body, 32<<20)) if err != nil { return nil, err } switch { case res.StatusCode == http.StatusNotFound: return nil, fmt.Errorf("%s %s: %w", method, path, errNotFound) case res.StatusCode >= 300: said := strings.TrimSpace(string(body)) if len(said) > 300 { said = said[:300] + "…" } return nil, &statusError{method, path, res.StatusCode, said} } return body, nil } func (f *Forge) getJSON(ctx context.Context, path string, into any) error { body, err := f.do(ctx, http.MethodGet, path) if err != nil { return err } if err := json.Unmarshal(body, into); err != nil { return fmt.Errorf("the forge's answer to %s is not readable: %w", path, err) } return nil } // PackageVersion is one version of one package, as the forge lists it. type PackageVersion struct { Name string `json:"name"` Version string `json:"version"` CreatedAt time.Time `json:"created_at"` } const pageSize = 50 // Packages is every npm package version of the owner, all pages. func (f *Forge) Packages(ctx context.Context) ([]PackageVersion, error) { var all []PackageVersion for page := 1; page <= 1000; page++ { var batch []PackageVersion path := fmt.Sprintf("/api/v1/packages/%s?type=npm&page=%d&limit=%d", url.PathEscape(f.Owner), page, pageSize) if err := f.getJSON(ctx, path, &batch); err != nil { return nil, err } all = append(all, batch...) if len(batch) < pageSize { return all, nil } } return nil, errors.New("the forge listed more than 1000 pages of packages; stopped rather than read for ever") } func (f *Forge) versionPath(name, version string) string { return fmt.Sprintf("/api/v1/packages/%s/npm/%s/%s", url.PathEscape(f.Owner), url.PathEscape(name), url.PathEscape(version)) } // Size is the bytes of one version's files. func (f *Forge) Size(ctx context.Context, name, version string) (int64, error) { var files []struct { Size int64 `json:"size"` } if err := f.getJSON(ctx, f.versionPath(name, version)+"/files", &files); err != nil { return 0, err } var total int64 for _, file := range files { total += file.Size } return total, nil } // DistTags is the dist-tags of one package, from the registry's own metadata. func (f *Forge) DistTags(ctx context.Context, name string) (map[string]string, error) { var meta struct { DistTags map[string]string `json:"dist-tags"` } path := fmt.Sprintf("/api/packages/%s/npm/%s", url.PathEscape(f.Owner), url.PathEscape(name)) if err := f.getJSON(ctx, path, &meta); err != nil { return nil, err } return meta.DistTags, nil } // Delete removes one version from the registry. func (f *Forge) Delete(ctx context.Context, name, version string) error { _, err := f.do(ctx, http.MethodDelete, f.versionPath(name, version)) return err } // Repository is one repository on the forge, as far as reading its manifests needs. type Repository struct { FullName string `json:"full_name"` DefaultBranch string `json:"default_branch"` Empty bool `json:"empty"` } // Repositories is every repository the admin sees. func (f *Forge) Repositories(ctx context.Context) ([]Repository, error) { var all []Repository for page := 1; page <= 1000; page++ { var found struct { Data []Repository `json:"data"` } if err := f.getJSON(ctx, fmt.Sprintf("/api/v1/repos/search?page=%d&limit=%d", page, pageSize), &found); err != nil { return nil, err } all = append(all, found.Data...) if len(found.Data) < pageSize { return all, nil } } return nil, errors.New("the forge listed more than 1000 pages of repositories; stopped rather than read for ever") } func repoPath(fullName string) string { owner, name, _ := strings.Cut(fullName, "/") return url.PathEscape(owner) + "/" + url.PathEscape(name) } // Files is every file path on a branch, all pages of the forge's tree. func (f *Forge) Files(ctx context.Context, repo Repository) ([]string, error) { var paths []string seen := 0 for page := 1; page <= 1000; page++ { var tree struct { Tree []struct { Path string `json:"path"` Type string `json:"type"` } `json:"tree"` Truncated bool `json:"truncated"` TotalCount int `json:"total_count"` } path := fmt.Sprintf("/api/v1/repos/%s/git/trees/%s?recursive=true&page=%d&per_page=1000", repoPath(repo.FullName), url.PathEscape(repo.DefaultBranch), page) if err := f.getJSON(ctx, path, &tree); err != nil { return nil, err } for _, e := range tree.Tree { if e.Type == "blob" { paths = append(paths, e.Path) } } seen += len(tree.Tree) if len(tree.Tree) == 0 || !tree.Truncated && (tree.TotalCount == 0 || seen >= tree.TotalCount) { return paths, nil } } return nil, fmt.Errorf("%s has more than 1000 pages of files; not read whole", repo.FullName) } // Raw is one file's content on the repository's default branch. func (f *Forge) Raw(ctx context.Context, repo Repository, file string) ([]byte, error) { escaped := strings.Split(file, "/") for i := range escaped { escaped[i] = url.PathEscape(escaped[i]) } return f.do(ctx, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/raw/%s?ref=%s", repoPath(repo.FullName), strings.Join(escaped, "/"), url.QueryEscape(repo.DefaultBranch))) }