package api import ( "archive/tar" "bytes" "context" "database/sql" "encoding/json" "errors" "fmt" imageassets "git.misaka.ren/m1saka/dsh-web/image" "git.misaka.ren/m1saka/dsh-web/internal/store" "io" "net/http" "net/url" "regexp" "strings" "sync" "time" ) // Caller holds buildQueue; interrupted jobs cannot be reported as successful. func (a *API) ensureBuilds() error { if buildQueue.initialized[a.s.DB] { return nil } _, err := a.s.DB.Exec(`CREATE TABLE IF NOT EXISTS image_builds(id TEXT PRIMARY KEY,version TEXT NOT NULL,status TEXT NOT NULL,logs TEXT NOT NULL DEFAULT '',error TEXT NOT NULL DEFAULT '',image_id TEXT NOT NULL DEFAULT '',created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS image_build_requests(user_id TEXT NOT NULL,build_id TEXT PRIMARY KEY,created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS images(image_id TEXT PRIMARY KEY,version TEXT NOT NULL,recipe_rev TEXT,platform TEXT,tag TEXT NOT NULL DEFAULT '',status TEXT NOT NULL,built_at TEXT NOT NULL,usage_count INTEGER NOT NULL DEFAULT 0);`) if err != nil { return err } for _, q := range []string{ "CREATE INDEX IF NOT EXISTS image_builds_history ON image_builds(created_at DESC,id DESC)", "ALTER TABLE image_builds ADD COLUMN requested_by TEXT NOT NULL DEFAULT ''", "ALTER TABLE image_builds ADD COLUMN recipe_rev TEXT", "ALTER TABLE image_builds ADD COLUMN platform TEXT", "ALTER TABLE image_builds ADD COLUMN finished_at TEXT", "ALTER TABLE image_builds ADD COLUMN experimental INTEGER NOT NULL DEFAULT 0", "ALTER TABLE image_builds ADD COLUMN release_id TEXT NOT NULL DEFAULT ''", "ALTER TABLE image_builds ADD COLUMN source_repository TEXT NOT NULL DEFAULT ''", "ALTER TABLE image_builds ADD COLUMN source_commit TEXT NOT NULL DEFAULT ''", "CREATE TABLE IF NOT EXISTS image_admissions(image_id TEXT NOT NULL,version TEXT NOT NULL,recipe_rev TEXT NOT NULL,platform TEXT NOT NULL,state TEXT NOT NULL,evidence TEXT NOT NULL,admitted_by TEXT NOT NULL,admitted_at TEXT NOT NULL,PRIMARY KEY(image_id,version,recipe_rev,platform))", "ALTER TABLE images ADD COLUMN recipe_rev TEXT", "ALTER TABLE images ADD COLUMN platform TEXT", "ALTER TABLE images ADD COLUMN usage_count INTEGER NOT NULL DEFAULT 0", } { if _, e := a.s.DB.Exec(q); e != nil && !strings.Contains(strings.ToLower(e.Error()), "duplicate column") { return e } } _, err = a.s.DB.Exec("DROP INDEX IF EXISTS image_builds_version; DROP INDEX IF EXISTS image_builds_identity; CREATE UNIQUE INDEX image_builds_identity ON image_builds(version,recipe_rev,platform) WHERE release_id='' AND recipe_rev IS NOT NULL AND platform IS NOT NULL AND status IN ('queued','running','validating','success'); CREATE UNIQUE INDEX IF NOT EXISTS image_builds_release_identity ON image_builds(source_repository,source_commit,release_id,platform) WHERE release_id<>'' AND status IN ('queued','running','validating','success')") if err != nil { return err } if _, err = a.s.DB.Exec("UPDATE image_builds SET status='failed',error='version requires administrator experimental build' WHERE status='queued' AND release_id='' AND experimental=0 AND version NOT IN ('0.2.0-rc.1','0.2.0-rc.2')"); err != nil { return err } if !buildQueue.initialized[a.s.DB] { if _, err = a.s.DB.Exec("UPDATE image_builds SET status='failed',error='image template identity is unknown' WHERE status='queued' AND (recipe_rev IS NULL OR recipe_rev<>? OR platform IS NULL OR platform<>?)", imageRecipeRev, imagePlatform); err != nil { return err } if _, err = a.s.DB.Exec("UPDATE image_builds SET status='failed',error='build interrupted by process restart' WHERE status IN ('running','validating')"); err != nil { return err } buildQueue.initialized[a.s.DB] = true } return nil } func (a *API) builds(w http.ResponseWriter, r *http.Request) { u, _, authErr := a.user(r) if authErr != nil { fail(w, 401, "authentication required") return } if r.Method != http.MethodGet && r.Method != http.MethodPost { fail(w, 405, "method not allowed") return } var in struct { Version string `json:"version"` Experimental bool `json:"experimental"` } if r.Method == http.MethodPost { dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)) dec.DisallowUnknownFields() if dec.Decode(&in) != nil || dec.Decode(new(any)) != io.EOF || !validBuildVersion(in.Version) { fail(w, 400, "a fixed version is required") return } } if r.Method == http.MethodGet { a.listBuilds(w, r) return } if in.Experimental && !u.Admin { fail(w, 403, "administrator required") return } b, err := a.enqueueBuild(r.Context(), u.ID, in.Version, in.Experimental) if err != nil { status := 502 if errors.Is(err, errBuildUnsupported) { status = 400 } if errors.Is(err, errBuildRateLimited) { status = 429 w.Header().Set("Retry-After", "3600") } fail(w, status, sanitizeBuildLog(err.Error())) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) writeJSON(w, b) } var errBuildRateLimited = errors.New("build limit: three new tasks per user per hour") // lookupActiveBuild returns the active or successful record for a version; it // performs no I/O and must be called with the build queue held. func (a *API) lookupActiveBuild(version string) (buildRecord, bool, error) { var b buildRecord var finished, recipe, platform sql.NullString err := a.s.DB.QueryRow("SELECT id,version,status,logs,error,image_id,created_at,requested_by,recipe_rev,platform,finished_at,experimental FROM image_builds WHERE release_id='' AND version=? AND recipe_rev=? AND platform=? AND status IN ('queued','running','validating','success')", version, imageRecipeRev, imagePlatform).Scan(&b.ID, &b.Version, &b.Status, &b.Logs, &b.Error, &b.ImageID, &b.Created, &b.RequestedBy, &recipe, &platform, &finished, &b.Experimental) b.RecipeRev, b.Platform, b.Finished = recipe.String, platform.String, finished.String if errors.Is(err, sql.ErrNoRows) { return b, false, nil } if err != nil { return b, false, err } return b, true, nil } func (a *API) enqueueBuild(ctx context.Context, uid, version string, experiment ...bool) (buildRecord, error) { experimental := len(experiment) > 0 && experiment[0] if experimental { u, err := a.s.UserByID(uid) if err != nil || !u.Admin { return buildRecord{}, errors.New("administrator required") } } if !experimental && !normalBuildAllowed(version) { return buildRecord{}, errBuildUnsupported } if !validBuildVersion(version) { return buildRecord{}, errors.New("a fixed version is required") } buildQueue.Lock() err := a.ensureBuilds() var b buildRecord var found bool if err == nil { b, found, err = a.lookupActiveBuild(version) } buildQueue.Unlock() if err != nil { return b, err } if found && !experimental && b.Experimental && b.Status != "success" { return b, errBuildUnsupported } if found && b.Status != "success" { a.startBuildWorker() return b, nil } if found { // Verifying a previously built image talks to Docker; keep that I/O // out of the queue lock shared with instance creation and upgrades. verify, cancel := context.WithTimeout(ctx, 15*time.Second) id, lookupErr := a.localBuildImage(verify, b.Version, b.ImageID) cancel() if lookupErr != nil { return b, lookupErr } if id != "" && id == b.ImageID { if !experimental { im, e := a.evaluateImage(ctx, id) if e != nil { return b, e } if !im.Selectable { return b, errBuildUnsupported } } a.startBuildWorker() return b, nil } buildQueue.Lock() if _, err = a.s.DB.Exec("UPDATE image_builds SET status='missing',error='image no longer available' WHERE id=? AND status='success'", b.ID); err == nil { // Another request may have queued a replacement while the image // was being verified; honor it instead of inserting a duplicate. b, found, err = a.lookupActiveBuild(version) } if err == nil && !found { b, err = a.insertBuild(uid, version, experimental) } buildQueue.Unlock() if err != nil { return b, err } if !experimental && found && b.Experimental { return b, errBuildUnsupported } a.startBuildWorker() return b, nil } buildQueue.Lock() defer buildQueue.Unlock() // Re-check after the unlocked phases: another request may have inserted. b, found, err = a.lookupActiveBuild(version) if err != nil { return b, err } if found { if !experimental && b.Experimental { return b, errBuildUnsupported } return b, nil } b, err = a.insertBuild(uid, version, experimental) if err != nil { return b, err } a.startBuildWorkerLocked() return b, nil } // insertBuild applies the per-user rate limit and inserts a queued record; // call with the build queue held. func (a *API) insertBuild(uid, version string, experiment ...bool) (buildRecord, error) { var count int if err := a.s.DB.QueryRow("SELECT count(*) FROM image_build_requests WHERE user_id=? AND created_at>?", uid, time.Now().Add(-time.Hour).UTC().Format(time.RFC3339Nano)).Scan(&count); err != nil { return buildRecord{}, err } if count >= 3 { return buildRecord{}, errBuildRateLimited } b := buildRecord{ID: store.ID(), Version: version, Status: "queued", Created: time.Now().UTC().Format(time.RFC3339Nano), RequestedBy: uid, RecipeRev: imageRecipeRev, Platform: imagePlatform} b.Experimental = len(experiment) > 0 && experiment[0] return b, a.insertBuildRequest(b, uid) } func (a *API) startBuildWorker() { buildQueue.Lock() defer buildQueue.Unlock() a.startBuildWorkerLocked() } // startBuildWorkerLocked requires the caller to hold buildQueue: the active map // is only safe under that lock. func (a *API) startBuildWorkerLocked() { if buildQueue.active[a.s.DB] { return } buildQueue.active[a.s.DB] = true go func() { var lastCleanupAudit time.Time var nextCleanup time.Time var recoveryErr error for { select { case <-a.s.Done(): buildQueue.Lock() delete(buildQueue.active, a.s.DB) buildQueue.Unlock() return default: } if time.Now().After(nextCleanup) { buildWorker.Lock() recovery, cancel := context.WithTimeout(context.Background(), 20*time.Second) recoveryErr = a.recoverBuildChecks(recovery) cancel() buildWorker.Unlock() if errors.Is(recoveryErr, errBuildCheckUncertain) { nextCleanup = time.Now().Add(time.Minute) } } if recoveryErr != nil { if time.Since(lastCleanupAudit) >= time.Minute { a.s.AddAudit("", "build.cleanup_error", "", sanitizeBuildLog(recoveryErr.Error())) lastCleanupAudit = time.Now() } } buildQueue.Lock() var b buildRecord row := a.s.DB.QueryRow("SELECT " + buildSummaryColumns + " FROM image_builds WHERE status='queued' ORDER BY created_at,id LIMIT 1") err := scanBuildSummary(row, &b) if err == nil { _, err = a.s.DB.Exec("UPDATE image_builds SET status='running' WHERE id=?", b.ID) } if err != nil { if !errors.Is(err, sql.ErrNoRows) || recoveryErr != nil { buildQueue.Unlock() select { case <-a.s.Done(): case <-time.After(time.Second): } continue } delete(buildQueue.active, a.s.DB) buildQueue.Unlock() return } buildQueue.Unlock() a.runImageBuild(b) nextCleanup = time.Time{} } }() } func imageBuildContext() (io.Reader, error) { var buf bytes.Buffer tw := tar.NewWriter(&buf) for _, name := range []string{"Dockerfile", "Caddyfile", "entrypoint.sh", "install-mobile.mjs", "install-free-search.mjs", "install-workspace.mjs", "init-profile.mjs"} { dataBytes, err := imageassets.Files.ReadFile(name) if err != nil { return nil, err } data := string(dataBytes) if err := tw.WriteHeader(&tar.Header{Name: name, Mode: 0755, Size: int64(len(data))}); err != nil { return nil, err } if _, err := tw.Write([]byte(data)); err != nil { return nil, err } } if err := tw.Close(); err != nil { return nil, err } return &buf, nil } func (a *API) runImageBuild(b buildRecord) { buildWorker.Lock() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) defer cancel() logs := "" rawLogs := "" var logErr error appendLog := func(s string) { if len(rawLogs) < buildLogLimit { remaining := buildLogLimit - len(rawLogs) if len(s) > remaining { s = s[:remaining] } rawLogs += s } logs = sanitizeBuildLog(rawLogs) if logErr == nil { _, logErr = a.s.DB.Exec("UPDATE image_builds SET logs=? WHERE id=?", logs, b.ID) } } tarball, err := imageBuildContext() if b.ReleaseID == "" && !b.Experimental && !normalBuildAllowed(b.Version) { err = errBuildUnsupported } if b.Experimental { u, e := a.s.UserByID(b.RequestedBy) if e != nil || !u.Admin { err = errors.New("experimental build requester is not an administrator") } } var id string if b.ReleaseID != "" { tarball, err = a.releaseBuildContext(ctx, b) } if err == nil && b.ReleaseID == "" { id, err = a.localBuildImage(ctx, b.Version, "") } if err == nil && id == "" { _, err = fetchNPMVersion(ctx, npmBuildClient, b.Version) if err == nil { err = a.checkBuildDisk(ctx) } } if err == nil && id == "" { if b.ReleaseID == "" { id, err = a.docker.Build(ctx, tarball, b.Version, "dsh-web:"+b.Version+"-"+imageRecipeRev, appendLog) } else { id, err = a.docker.BuildRelease(ctx, tarball, b.Version, "dsh-web:release-"+b.SourceCommit, b.ReleaseID, b.SourceRepository, b.SourceCommit, appendLog) } } if err == nil { var matches bool matches, err = a.buildImageMatches(ctx, id, b) if err == nil && !matches { err = errors.New("image template or platform mismatch") } } if err == nil { _, err = a.s.DB.Exec("UPDATE image_builds SET status='validating',image_id=? WHERE id=?", id, b.ID) } if err == nil { err = a.verifyBuiltImage(ctx, id, b.Version, b.ID) } if err == nil { err = logErr } status, detail := "success", "" if err != nil { status = "failed" detail = sanitizeBuildLog(err.Error()) appendLog(string(rune(10)) + detail) } finished := time.Now().UTC().Format(time.RFC3339Nano) buildWorker.Unlock() if err := a.persistBuildResult(b, status, logs, detail, id, finished); err != nil { go a.finishBuild(b, status, logs, detail, id, finished) } } func (a *API) verifyBuiltImage(parent context.Context, image, version string, buildIDs ...string) (result error) { ctx, cancel := context.WithTimeout(parent, 90*time.Second) defer cancel() id := store.ID() if len(buildIDs) != 0 { id = buildIDs[0] } if err := a.ensureBuildChecks(); err != nil { return err } if _, err := a.s.DB.Exec("INSERT INTO image_build_checks(build_id,image_id,created_at) VALUES(?,?,?)", id, image, time.Now().UTC().Format(time.RFC3339Nano)); err != nil { return err } var created struct { ID string `json:"Id"` } rejected := false defer func() { cleanup, c := context.WithTimeout(context.Background(), 10*time.Second) defer c() var err error if rejected { _, err = a.s.DB.Exec("DELETE FROM image_build_checks WHERE build_id=?", id) } else if created.ID != "" { err = a.deleteBuildCheck(cleanup, id, created.ID) } else { err = a.cleanupBuildCheck(cleanup, id, image) } result = errors.Join(result, err) }() config := map[string]any{"Image": image, "User": "1000:1000", "Labels": map[string]string{"dsh-mgr.role": "build-check", "dsh-mgr.build": id}, "HostConfig": map[string]any{"NetworkMode": "none", "ReadonlyRootfs": true, "CapDrop": []string{"ALL"}, "SecurityOpt": []string{"no-new-privileges:true"}, "Memory": int64(1 << 30), "NanoCpus": int64(1e9), "PidsLimit": 256, "Tmpfs": map[string]string{"/tmp": "rw,exec,nosuid,nodev,size=256m,mode=1777", "/workspace": "rw,nosuid,nodev,size=256m,uid=1000,gid=1000"}}} if err := a.docker.Do(ctx, http.MethodPost, "/containers/create?name="+url.QueryEscape(buildCheckName(id)), config, &created); err != nil { rejected = dockerCreateRejected(err) return err } if created.ID == "" { return errors.New("verification container ID missing") } if _, err := a.s.DB.Exec("UPDATE image_build_checks SET container_id=? WHERE build_id=?", created.ID, id); err != nil { return err } path := "/containers/" + url.PathEscape(created.ID) if err := a.docker.Do(ctx, http.MethodPost, path+"/start", nil, nil); err != nil { return err } script := `const end=Date.now()+60000;async function probe(){try{const r=await fetch('http://127.0.0.1:8080/',{redirect:'manual',signal:AbortSignal.timeout(2000)});if(r.status>=200&&r.status<500)process.exit(0);}catch{}if(Date.now()>end)process.exit(1);setTimeout(probe,500);}probe();` { expected, _ := json.Marshal(version) script = `const cp=require('node:child_process');const actual=cp.execFileSync('dsh',['--version'],{encoding:'utf8',timeout:20000}).trim();if(actual!==` + string(expected) + `)process.exit(2);` + script } script = `const fs=require('node:fs');for(const [k,v] of Object.entries({DSH_HOME:'/workspace',HOME:'/workspace',XDG_DATA_HOME:'/workspace/.local/share',XDG_CONFIG_HOME:'/workspace/.config'})){if(process.env[k]!==v)process.exit(3);}if(!fs.lstatSync('/data/dsh-home').isSymbolicLink()||fs.realpathSync('/data/dsh-home')!=='/workspace')process.exit(4);fs.writeFileSync('/workspace/.home-check','isolated');if(fs.readFileSync('/data/dsh-home/.home-check','utf8')!=='isolated')process.exit(5);fs.unlinkSync('/workspace/.home-check');` + script var exec struct { ID string `json:"Id"` } if err := a.docker.Do(ctx, http.MethodPost, path+"/exec", map[string]any{"Cmd": []string{"node", "-e", script}, "User": "1000:1000"}, &exec); err != nil { return err } if exec.ID == "" { return errors.New("verification exec ID missing") } ep := "/exec/" + url.PathEscape(exec.ID) if err := a.docker.Do(ctx, http.MethodPost, ep+"/start", map[string]bool{"Detach": true, "Tty": false}, nil); err != nil { return err } ticker := time.NewTicker(250 * time.Millisecond) defer ticker.Stop() for { var state struct { Running bool ExitCode int } if err := a.docker.Do(ctx, http.MethodGet, ep+"/json", nil, &state); err != nil { return err } if !state.Running { if state.ExitCode != 0 { return fmt.Errorf("image startup verification failed (exit %d)", state.ExitCode) } container, err := a.docker.Inspect(ctx, created.ID) if err != nil { return err } if !container.State.Running { return errors.New("verification container exited") } return nil } select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: } } } const buildLogLimit = 64 << 10 var fixedVersion = regexp.MustCompile(`^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(-[0-9A-Za-z-]+([.][0-9A-Za-z-]+)*)?$`) var buildSecrets = regexp.MustCompile(`(?i)(authorization["']?[[:space:]]*[:=][[:space:]]*["']?(?:bearer[[:space:]]+|basic[[:space:]]+)?|(?:token|password|passwd|secret|api[_-]?key|cookie)["']?[[:space:]]*[:=][[:space:]]*["']?)[^[:space:]"'&,;]+`) var buildURLCredentials = regexp.MustCompile(`(https?://)[^/@[:space:]]+:[^/@[:space:]]+@`) var buildQueue = struct { sync.Mutex initialized map[*sql.DB]bool active map[*sql.DB]bool }{initialized: map[*sql.DB]bool{}, active: map[*sql.DB]bool{}} var buildWorker sync.Mutex type imageRecord struct { Admission string `json:"admission"` AdmissionReason string `json:"admission_reason"` Selectable bool `json:"selectable"` ID string `json:"id"` ImageID string `json:"image_id"` Version string `json:"version"` Tag string `json:"tag,omitempty"` Status string `json:"status"` Created string `json:"created_at"` RecipeRev string `json:"recipe_rev,omitempty"` Platform string `json:"platform,omitempty"` UsageCount int `json:"usage_count"` } type buildRecord struct { ReleaseID string `json:"release_id,omitempty"` SourceRepository string `json:"source_repository,omitempty"` SourceCommit string `json:"source_commit,omitempty"` Experimental bool `json:"experimental"` ImageID string `json:"image_id,omitempty"` ID string `json:"id"` Version string `json:"version"` Status string `json:"status"` Logs string `json:"logs,omitempty"` Error string `json:"error,omitempty"` Created string `json:"created_at"` RequestedBy string `json:"requested_by,omitempty"` RecipeRev string `json:"recipe_rev,omitempty"` Platform string `json:"platform,omitempty"` Finished string `json:"finished_at,omitempty"` } func (a *API) images(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodDelete { a.deleteImage(w, r) return } if _, _, err := a.user(r); err != nil { fail(w, 401, "authentication required") return } if r.Method != http.MethodGet { fail(w, 405, "method not allowed") return } ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() result, err := a.imageIndex(ctx) if err != nil { fail(w, 502, "image index reconciliation failed") return } writeJSON(w, result) } // verifiedImagesTTL bounds how long a cached admission evaluation is reused // by the one-second intent poller. var verifiedImagesTTL = 5 * time.Second type evalCacheEntry struct { im imageRecord at time.Time } // evaluateImageCached reuses recent per-image evaluations for the intent // poller so repeated ticks do not hammer Docker. Deletions and admission // writes bump imageDeleteGeneration and invalidate every entry. func (a *API) evaluateImageCached(ctx context.Context, gen uint64, id string) (imageRecord, error) { a.evalCacheMu.Lock() if entry, ok := a.evalCache[id]; ok && a.evalCacheGen == gen && time.Since(entry.at) < verifiedImagesTTL { a.evalCacheMu.Unlock() return entry.im, nil } a.evalCacheMu.Unlock() im, err := a.evaluateImage(ctx, id) if err != nil { return im, err } a.evalCacheMu.Lock() if a.evalCache == nil || a.evalCacheGen != gen { a.evalCache, a.evalCacheGen = make(map[string]evalCacheEntry), gen } a.evalCache[id] = evalCacheEntry{im: im, at: time.Now()} a.evalCacheMu.Unlock() return im, nil } func (a *API) successfulImageIDs(ctx context.Context) ([]string, error) { rows, err := a.s.DB.QueryContext(ctx, "SELECT DISTINCT image_id FROM image_builds WHERE status='success' AND image_id<>''") if err != nil { return nil, err } ids := []string{} for rows.Next() { var id string if err = rows.Scan(&id); err != nil { rows.Close() return nil, err } ids = append(ids, id) } err = rows.Err() rows.Close() if err != nil { return nil, err } return ids, nil } func (a *API) verifiedImages(ctx context.Context) (map[string]string, error) { buildQueue.Lock() err := a.ensureBuilds() buildQueue.Unlock() if err != nil { return nil, err } ids, err := a.successfulImageIDs(ctx) if err != nil { return nil, err } result := map[string]string{} for _, id := range ids { im, err := a.evaluateImage(ctx, id) if err != nil { return nil, err } if im.Selectable { result[id] = im.Version } } return result, nil } // verifiedImagesPoll is the intent poller's view: build rows are reread every // tick, but per-image Docker evaluation is cached briefly so a stuck waiting // intent cannot turn the one-second loop into Docker hammering. func (a *API) verifiedImagesPoll(ctx context.Context) (map[string]string, error) { buildQueue.Lock() err := a.ensureBuilds() gen := a.imageDeleteGeneration buildQueue.Unlock() if err != nil { return nil, err } ids, err := a.successfulImageIDs(ctx) if err != nil { return nil, err } result := map[string]string{} for _, id := range ids { im, err := a.evaluateImageCached(ctx, gen, id) if err != nil { return nil, err } if im.Selectable { result[id] = im.Version } } return result, nil } func (a *API) latest(w http.ResponseWriter, r *http.Request) { if _, _, err := a.user(r); err != nil { fail(w, 401, "authentication required") return } if r.Method != http.MethodGet { fail(w, 405, "method not allowed") return } v, err := fetchLatest(r.Context(), npmBuildClient) if err != nil { fail(w, 502, "npm version lookup failed") return } writeJSON(w, map[string]any{"version": v, "checked_at": time.Now().UTC().Format(time.RFC3339), "supported": normalBuildAllowed(v), "build_allowed": normalBuildAllowed(v), "admission": defaultAdmission(v, imageRecipeRev, imagePlatform), "admission_reason": "normal builds require a built-in tested tuple; unknown versions require administrator experimental validation and admission"}) } func fetchLatest(ctx context.Context, client *http.Client) (string, error) { return fetchNPMVersion(ctx, client, "latest") } func fetchNPMVersion(ctx context.Context, client *http.Client, version string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://registry.npmjs.org/@deepseek-ai%2Fdsh/"+url.PathEscape(version), nil) if err != nil { return "", err } resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != 200 { return "", fmt.Errorf("npm status %d", resp.StatusCode) } var v struct{ Version string } if err = json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&v); err != nil { return "", err } if !validBuildVersion(v.Version) || (version != "latest" && v.Version != version) { return "", errors.New("invalid npm version") } return v.Version, nil } func validBuildVersion(v string) bool { return len(v) <= 100 && fixedVersion.MatchString(v) } func sanitizeBuildLog(s string) string { s = buildURLCredentials.ReplaceAllString(s, "$1[REDACTED]@") s = buildSecrets.ReplaceAllString(s, "$1[REDACTED]") s = strings.ToValidUTF8(s, "") if len(s) > buildLogLimit { s = strings.ToValidUTF8(s[:buildLogLimit], "") } return s } func (a *API) localBuildImage(ctx context.Context, version, preferred string) (string, error) { filters, _ := json.Marshal(map[string][]string{"label": {"dsh-mgr.role=runtime", "dsh-mgr.version=" + version}}) var images []struct { ID string Labels map[string]string } if err := a.docker.Do(ctx, http.MethodGet, "/images/json?filters="+url.QueryEscape(string(filters)), nil, &images); err != nil { return "", err } for _, im := range images { if preferred == "" && im.Labels["dsh-mgr.release"] != "" { continue } if im.ID != "" && im.Labels["dsh-mgr.role"] == "runtime" && im.Labels["dsh-mgr.version"] == version && im.Labels["dsh-mgr.recipe"] == imageRecipeRev && (preferred == "" || im.ID == preferred) { ok, err := a.imageMatches(ctx, im.ID, version) if err != nil { return "", err } if ok { return im.ID, nil } } } return "", nil }