package packageguard import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io/fs" "os" "path/filepath" "sort" "strings" "testing" ) // The registry refuses a second, different package under one version // ("version taken", issues 1899 and 1331). This guard fails before a merge when // what npm packs changes while the version stays: package-version.sum holds the // version and a sha256 over the inputs of the packed dist (package.json without // its version, src/, tsconfig.json) and of the README npm always packs. // The lock file is not packed, so it is not hashed. const sumFile = "package-version.sum" func packedInputs(root string) (version, sum string, err error) { raw, err := os.ReadFile(filepath.Join(root, "package.json")) if err != nil { return "", "", err } var pkg map[string]any if err := json.Unmarshal(raw, &pkg); err != nil { return "", "", err } version, _ = pkg["version"].(string) delete(pkg, "version") canon, err := json.Marshal(pkg) // map keys are sorted: stable bytes if err != nil { return "", "", err } files := map[string][]byte{"package.json": canon} for _, name := range []string{"tsconfig.json", "README.md"} { b, err := os.ReadFile(filepath.Join(root, name)) if err != nil { return "", "", err } files[name] = b } err = filepath.WalkDir(filepath.Join(root, "src"), func(p string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return err } b, err := os.ReadFile(p) if err != nil { return err } rel, _ := filepath.Rel(root, p) files[filepath.ToSlash(rel)] = b return nil }) if err != nil { return "", "", err } names := make([]string, 0, len(files)) for n := range files { names = append(names, n) } sort.Strings(names) h := sha256.New() for _, n := range names { fmt.Fprintf(h, "%s\x00%d\x00", n, len(files[n])) h.Write(files[n]) } return version, hex.EncodeToString(h.Sum(nil)), nil } func TestPackageVersionMatchesPackedInputs(t *testing.T) { root := "../.." if r := os.Getenv("MESH_SDK_ROOT"); r != "" { root = r } version, sum, err := packedInputs(root) if err != nil { t.Fatal(err) } line := version + " " + sum + "\n" if os.Getenv("UPDATE_PACKAGE_VERSION_SUM") != "" { if err := os.WriteFile(filepath.Join(root, sumFile), []byte(line), 0o644); err != nil { t.Fatal(err) } return } raw, err := os.ReadFile(filepath.Join(root, sumFile)) if err != nil { t.Fatalf("%s is missing: %v", sumFile, err) } recVersion, recSum, _ := strings.Cut(strings.TrimSpace(string(raw)), " ") switch { case recVersion == version && recSum == sum: case recVersion == version: t.Fatalf("what npm packs for @novox/mesh-sdk changed while its version stayed %s: the registry would refuse a second, different %s. Raise @novox/mesh-sdk's version (package.json and package-lock.json) and update %s (UPDATE_PACKAGE_VERSION_SUM=1 go test -run TestPackageVersionMatchesPackedInputs in go/packageguard)", version, version, sumFile) default: t.Fatalf("package.json says %s but %s records %s: update %s (UPDATE_PACKAGE_VERSION_SUM=1 go test -run TestPackageVersionMatchesPackedInputs in go/packageguard)", version, sumFile, recVersion, sumFile) } }