package builder import ( "encoding/base64" "fmt" "net/url" "strings" ) // Npmrc is what a build needs to resolve the mesh's own shared library — and any other package the // mesh publishes — from the mesh's package registry rather than from a git URL (novox/hq ADR 0076, // issue 053). // // It is a build-time credential, not a runtime one. A module compiled inside the toolchain image // resolves the SDK there, once, when that image is built; the running container never speaks to the // package registry. So this is given to the *builder*, the way the artifact store is // (`whereToPublish`), and reaches a build as a secret rather than a layer — see Secret. // // The registry's exact URL shape is the provider's business, not the builder's: it arrives whole, // either from the binding the mesh writes (a package-registry provider's `serves` facts) or from the // environment when a person runs a build by hand. Nothing here knows gitea from verdaccio. type Npmrc struct { // Scope is the npm scope the registry answers for, e.g. "@novox". A build resolves only this // scope from the mesh; everything else resolves the ordinary way, so a mesh with no internet // still cannot pull the public registry's version of a name the mesh also publishes. Scope string // Registry is the full base URL a client uses for this scope, e.g. // "https:///api/packages//npm/". Trailing slash tolerated either way. Registry string // Token authenticates to the registry as a bearer token, when a provider mints one. Left empty // when the mesh authenticates the ordinary way it authenticates everything — a generated // password it applies and seals — for which see Username and Password. Token string // Username and Password authenticate by basic auth, which is what gitea and verdaccio both // accept and what lets the credential be a mesh-generated password the provider's provisioner // applies and the mesh seals to the consumer — the same shape a database password takes. The // username is the consumer's mesh identity. Ignored when Token is set. Username string Password string } // Enabled reports whether there is a registry to resolve against at all. A bootstrap build that // runs before any package registry exists has none, and must still build whatever needs no // mesh-published dependency. func (n Npmrc) Enabled() bool { return strings.TrimSpace(n.Scope) != "" && strings.TrimSpace(n.Registry) != "" } // File renders the .npmrc a build mounts. Two lines: which registry answers for the scope, and the // token to present to it. The auth line is keyed by the registry URL with its scheme removed, which // is how npm matches a stored credential to a request. // // It returns an error rather than a malformed file, because an .npmrc that npm parses but points // nowhere fails much later, inside a build, as a package that cannot be found. func (n Npmrc) File() (string, error) { scope := strings.TrimSpace(n.Scope) if !strings.HasPrefix(scope, "@") { return "", fmt.Errorf("a package-registry scope is written with its leading @, not %q", scope) } reg := strings.TrimSpace(n.Registry) if !strings.HasPrefix(reg, "http://") && !strings.HasPrefix(reg, "https://") { return "", fmt.Errorf("a package registry is reached over http(s), and %q is neither", reg) } if !strings.HasSuffix(reg, "/") { // npm's per-scope registry key is matched by prefix, and the auth key below is derived from // it; a missing trailing slash makes the two disagree and the token is never sent. reg += "/" } parsed, err := url.Parse(reg) if err != nil { return "", fmt.Errorf("%q is not a usable registry URL: %w", reg, err) } // The auth key is the URL without its scheme, e.g. "//host/api/packages/owner/npm/". authKey := "//" + parsed.Host + parsed.EscapedPath() var auth string switch { case strings.TrimSpace(n.Token) != "": auth = fmt.Sprintf("%s:_authToken=%s\n", authKey, strings.TrimSpace(n.Token)) case strings.TrimSpace(n.Username) != "" && n.Password != "": // npm reads the password base64-encoded, and always-auth so it presents the credential to // reads as well as writes — a private registry answers neither without it. enc := base64.StdEncoding.EncodeToString([]byte(n.Password)) auth = fmt.Sprintf("%s:username=%s\n%s:_password=%s\n%s:always-auth=true\n", authKey, strings.TrimSpace(n.Username), authKey, enc, authKey) default: return "", fmt.Errorf( "the package registry at %s was given neither a token nor a username and password", reg) } return fmt.Sprintf("%s:registry=%s\n%s", scope, reg, auth), nil } // packageRecipe is how a `package` artifact is built and published: on a PUBLIC base image, never // the mesh toolchain, because the toolchain is built from the package this produces (the SDK). The // script builds the module, then publishes it to the mesh's package registry unless that exact // version is already there — so a re-run of genesis, which must be safe, does not fail on a version // it published a moment ago. type packageRecipe struct { Base string Script string } var packageRecipes = map[string]packageRecipe{ "typescript": { Base: "node:22-bookworm-slim", Script: `set -e npm install --no-audit --no-fund npm run build name="$(node -p "require('./package.json').name")" ver="$(node -p "require('./package.json').version")" if npm view "$name@$ver" version >/dev/null 2>&1; then echo "mesh-builder: $name@$ver is already published, leaving it" else npm publish fi`, }, } // PackageLanguages is the languages a package artifact can be written in, for a manifest check that // wants to refuse one it cannot build before a build starts. func PackageLanguages() []string { out := make([]string, 0, len(packageRecipes)) for l := range packageRecipes { out = append(out, l) } return out }