A kódbázis kommentek nélkül marad

Kérésre: minden magyarázó komment kikerült a forrásfájlokból — 89 Ruby, 16
TypeScript, 14 Vue, plusz a CSS/JS/CJS. Nem soralapú kereséssel: a Ruby-t a
Ripper tokenizálta, a JS/TS/CSS-t állapotgép járta végig, hogy az URL-ekben,
reguláris kifejezésekben és heredocokban álló // és # jelek helyükön
maradjanak.

Három komment maradt, mert nélkülük nem indul a kód: az entrypoint.sh
shebangja, a vite-env.d.ts hármas perjeles referenciája, és a sanitize
teszt @vitest-environment direktívája (ez utóbbi a magyarázó része nélkül).

Egy helyen kódot is kellett írni: a CommandBlock másolás-hibaágán a komment
volt a catch egyetlen tartalma, és üres blokkot az eslint nem enged — a
copied jelző visszaállítása került a helyére.

A yaml, Dockerfile, Makefile, erb és markdown fájlokat nem érintettem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-20 12:52:47 +02:00
co-authored by Claude Opus 5
parent 4505571052
commit a0fbf1e2b4
63 changed files with 46 additions and 634 deletions
@@ -1,14 +1,7 @@
module WarpEngine
module Platforms
module Builds
# Linux on 64-bit ARM: Raspberry Pi 4/5, Odroid, and the retro handhelds.
# Batocera and the ES-family distributions run on these as much as on
# x86_64, and an x64 binary installs there but will not start — which is
# why this is a separate kind rather than something linux_x64 can cover.
#
# Include this in a platform service only once its pipeline actually
# produces the artifact: registering the kind makes /api/builds report it
# as missing for every release until then.
module BuildLinuxArm64
extend ActiveSupport::Concern
@@ -1,13 +1,10 @@
require "erb"
module WarpEngine
# Renders the /build/config platform templates: the pipeline logic lives in
# app/services/warp_engine/platforms/<platform>/pipeline.yaml.erb, the
# per-platform builder images come from WarpEngine.config.ci_platforms.
class CiConfigService
PLATFORM_FORMAT = /\A[a-z0-9_-]+\z/
# The rendered pipeline YAML, or nil when the platform is not served.
def render(platform:, name:, update_server:)
platform = platform.to_s
return nil unless platform.match?(PLATFORM_FORMAT)
@@ -4,12 +4,7 @@ require "net/http"
require "digest"
module WarpEngine
# Verifies the signature of Woodpecker configuration-extension requests.
# Woodpecker 3.x signs with RFC 9421 HTTP message signatures (ed25519, via
# yaronf/httpsign): Signature-Input + Signature + Content-Digest headers,
# covered components "@request-target" and "content-digest". Older versions
# used draft-cavage http-signatures (a single Signature header) — kept as a
# fallback.
class CiSignatureVerifier
CAVAGE_PARAM = /(\w+)="([^"]*)"/
@@ -17,7 +12,7 @@ module WarpEngine
@key_mutex = Mutex.new
class << self
# The downloaded key is cached process-wide (per URL).
def fetch_public_key(url)
@key_mutex.synchronize do
@key_cache[url] ||= Net::HTTP.get(URI.parse(url))
@@ -64,8 +59,6 @@ module WarpEngine
nil
end
# --- RFC 9421 ---
def rfc9421_valid?(key)
input = @request.headers["Signature-Input"].to_s
match = input.match(/\A\s*([\w.-]+)=(\(.*)\z/m)
@@ -102,8 +95,6 @@ module WarpEngine
end
end
# When content-digest is a covered component, the body itself must match
# the digest header — this is what ties the signature to the payload.
def content_digest_valid?(components)
return true unless components.include?("content-digest")
@@ -114,8 +105,6 @@ module WarpEngine
ActiveSupport::SecurityUtils.secure_compare(digest, expected)
end
# --- draft-cavage fallback ---
def cavage_valid?(key)
params = cavage_params
return false if params.nil? || params["signature"].blank?
@@ -126,7 +115,6 @@ module WarpEngine
key.verify(nil, Base64.decode64(params["signature"]), signing_string)
end
# Parameters of the Signature header (or the "Authorization: Signature ..." form).
def cavage_params
header = @request.headers["Signature"].presence
if header.nil?
@@ -1,25 +1,15 @@
module WarpEngine
# The device authorization grant, from both ends.
#
# The client's end is #request, #poll and #revoke, all reachable over /api/auth/*.
# The person's end is #approve and #deny, which the *host* calls from its own page —
# approving needs a session and a logged-in human, and the engine has neither.
class DeviceGrantService
class NotConfigured < StandardError; end
class UnknownCode < StandardError; end
# A client asks for a code pair. Deliberately unauthenticated: there is nobody to
# authenticate as yet, which is the whole reason this flow exists.
def request(client_name:)
ensure_configured!
WarpEngine::DeviceGrant.create!(client_name: client_name.presence&.truncate(128))
end
# The client polls with the device code. Returns [state, token], where the token is
# the plain string and is available exactly once — on the poll that finds the grant
# newly approved. A second poll gets :approved with no token, which is the honest
# answer: the secret was handed over and is not kept.
def poll(device_code:)
ensure_configured!
@@ -28,16 +18,11 @@ module WarpEngine
return [ grant.state, nil ] unless grant.state == :approved
# Read once, then gone: the column exists only to carry the secret across the gap
# between the browser that approved it and the client that is polling for it.
plain = grant.issued_token
grant.update_columns(issued_token: nil) if plain.present?
[ :approved, plain ]
end
# The host's approval page calls this with the code a person typed and the subject
# they are signed in as. Issuing the token here rather than on the next poll keeps
# the decision and its consequence in one transaction.
def approve(user_code:, subject:)
ensure_configured!
@@ -73,8 +58,6 @@ module WarpEngine
grant
end
# Signing out: the client throws its own token away. Revocation is a soft delete on
# the token, so the audit trail of who signed in from where survives it.
def revoke(token:)
return false if token.nil?
@@ -82,9 +65,6 @@ module WarpEngine
true
end
# Where a person goes to type the user code. A path is made absolute against the
# request's own base, so a host that configured "/devices" does not have to know its
# own hostname.
def verification_url(base_url: nil)
configured = WarpEngine.config.identity_verification_url.presence || "/devices"
return configured if configured.start_with?("http://", "https://")
+1 -17
View File
@@ -1,25 +1,16 @@
module WarpEngine
class DownloadService
# The access policy said no. The controller turns this into a 403 — distinct from
# the nil that means "no such file", because telling a person their file is missing
# when it is merely locked sends them looking for the wrong problem.
class Denied < StandardError; end
def self.container_base
WarpEngine.config.file_container_path
end
# Lazy: a container path csak az első használatkor kötelező, boot/teszt közben nem.
def self.base_path
Pathname.new(container_base).realpath
end
# A letöltés helyét adja vissza (fájl vagy aláírt URL) és naplózza a
# letöltést. A hely feloldása a storage adapteren megy — alapból :local,
# tehát változatlanul lemezről.
#
# `subject` is whoever the request authenticated as, or nil. Under the open policy
# it is ignored and every file is served, exactly as before.
def locate(path:, ip:, user_agent:, referer:, subject: nil, request: nil)
relative = path.to_s
return nil unless storage.file?(relative)
@@ -34,8 +25,6 @@ module WarpEngine
expires_in: grant.expires_in)
end
# Visszafelé kompatibilis felület: az abszolút fájlútvonalat adja vissza
# (vagy nil-t). Nem lemezes adapternél nincs útvonal — ott a #locate való.
def create(path:, ip:, user_agent:, referer:, subject: nil, request: nil)
location = locate(path: path, ip: ip, user_agent: user_agent, referer: referer,
subject: subject, request: request)
@@ -50,9 +39,6 @@ module WarpEngine
WarpEngine.storage
end
# A policy that refuses returns nil; one that raises is treated as a refusal too.
# An artifact served because the gatekeeper crashed is the one failure mode this
# engine must not have.
def authorize!(asset, subject, request)
grant = WarpEngine.access_policy.authorize_download(asset: asset, subject: subject, request: request)
raise Denied if grant.nil?
@@ -80,8 +66,6 @@ module WarpEngine
referer: referer&.truncate(500)
)
# The same seam the publish side has: a host that wants its own record of who
# downloaded what subscribes rather than reaching into this class.
ActiveSupport::Notifications.instrument("warp_engine.download",
path: relative, asset: asset, release: asset&.release,
software: asset&.release&.software, subject: subject, download: download, ip: ip)
@@ -1,6 +1,6 @@
module WarpEngine
class FileManagerService
# Lazy: a container path csak az első használatkor kötelező, boot/teszt közben nem.
def base_path
@base_path ||= Pathname.new(WarpEngine.config.file_container_path)
end
+2 -19
View File
@@ -1,22 +1,10 @@
module WarpEngine
class FileService
# Lazy: a container path csak az első használatkor kötelező, boot/teszt közben nem.
def self.base_path
Pathname.new(WarpEngine.config.file_container_path).realpath
end
# A fájlok helyét a storage adapter adja (alapból :local, azaz a lemez) —
# így a host az objektumtárból is kiszolgálhat anélkül, hogy az engine-t
# patchelné. Lásd WarpEngine::Storage.
# `subject` is whoever the request authenticated as, or nil. Under the open policy
# it is ignored and every file is served, exactly as before.
#
# A hosted (browser) build reaches this path as hundreds of relative requests for
# js, wasm and images, which is why the gate here is the policy's plain yes/no
# rather than anything signed: there is nothing to sign per file. A host serving
# gated web builds to browsers will usually want its own session-based route in
# front of this one — a browser has a session, and a redirect to a login page is a
# better answer there than a bare 403.
def show(input, subject: nil)
relative = input.path.to_s
@@ -36,13 +24,8 @@ module WarpEngine
private
# Same rule and same failure mode as DownloadService: a policy that refuses or
# raises means no file. An artifact served because the gatekeeper crashed is the
# one failure mode this engine must not have.
def authorize!(relative, subject)
# The open policy authorises everything, and this path serves a browser build as
# hundreds of requests for js, wasm and images. Asking it per file would mean a
# LIKE query per asset for an answer that is always yes.
return WarpEngine::Access::Grant::OPEN if WarpEngine::AccessPolicy.open?
asset = WarpEngine::ReleaseAsset.where("path LIKE ?", "%#{relative.gsub('%', '\\%').gsub('_', '\\_')}%").first
@@ -8,8 +8,6 @@ module WarpEngine
@client.trigger_pipeline(pipeline.woodpecker_repo_id, branch: branch)
end
# Fetches one page of a pipeline's runs; the newest one refreshes the
# cached last_pipeline_* columns (Pipelines index and the CI API).
def list_pipelines(pipeline, page: 1)
runs = @client.list_pipelines(pipeline.woodpecker_repo_id, page: page)
refresh_last_pipeline(pipeline, runs.first) if page == 1 && runs.is_a?(Array)
@@ -22,7 +20,6 @@ module WarpEngine
private
# Woodpecker returns unix epoch seconds in "created".
def refresh_last_pipeline(pipeline, run)
return unless run && pipeline.persisted?
+1 -6
View File
@@ -1,8 +1,6 @@
module WarpEngine
class PublishService
# Az esemény neve, amit a host lehallgathat. A payload:
# platform: String, name: String, version: String,
# software: WarpEngine::Software, release: WarpEngine::Release
NOTIFICATION = "warp_engine.publish".freeze
def publish(input)
@@ -13,9 +11,6 @@ module WarpEngine
release = "WarpEngine::Platforms::#{input.platform.camelize}::Service".constantize
.new.update(input.name, input.version)
# A publikálás az egyetlen pont, ahol új build kerül a katalógusba —
# a host innen tud rá reagálni (értesítés, feed, csatorna-előléptetés)
# anélkül, hogy modell-callbackre kellene kapaszkodnia.
ActiveSupport::Notifications.instrument(
NOTIFICATION,
platform: input.platform,
@@ -5,7 +5,7 @@ module WarpEngine
def build_response(software, releases, download_counts, subject: nil)
sorted = releases.sort_by { |r| r.created_at || Time.at(0) }.reverse
latest = sorted.reject { |r| r.version.to_s.start_with?("dev-") }.first
# web-playable, ha az utolsó (stabil) release-nek van webes assetje
web_playable = latest if latest&.release_assets&.any? { |a| a.kind == "html" }
total_downloads = releases.sum { |r| download_counts.fetch(r.id, 0) }
@@ -19,21 +19,14 @@ module WarpEngine
)
end
# Always present, even under the open policy: a client should never have to tell
# "this catalog says nothing about access" from "this title is not gated". One of
# those is a question and the other is an answer.
def access_for(software, subject)
WarpEngine.access_policy.access_for(software: software, subject: subject)
rescue StandardError => e
# A policy that raises must not take the catalog down with it. The open answer is
# wrong here, so the closed one is what a broken policy gets: a title nobody can
# download is recoverable, a paid title handed out for free is not.
Rails.logger.error("[WarpEngine::AccessPolicy] #{e.class}: #{e.message}")
WarpEngine::Access.new(gated: true, entitled: false)
end
# The titles this subject may see. A policy that raises empties the catalog rather
# than leaking it — the safe direction — but it is still a bug, so it is logged.
def visible_scope(subject)
WarpEngine.access_policy.visible_software_scope(subject: subject)
rescue StandardError => e
@@ -41,7 +34,6 @@ module WarpEngine
WarpEngine::Software.none
end
# Egyetlen csoportosított lekérdezés release-enkénti letöltésszámokhoz (N+1 helyett).
def download_counts_for(release_ids)
return {} if release_ids.empty?
WarpEngine::Download.where(release_id: release_ids).group(:release_id).count
@@ -2,9 +2,6 @@ module WarpEngine
class SoftwareService
include SoftwareResponseBuilder
# `subject` is whoever the request authenticated as, or nil. It decides two things:
# which titles are listed at all (the policy's scope) and what each one's `access`
# block says about entitlement.
def index(owner_id: nil, subject: nil)
softwares = visible_scope(subject)
.includes(releases: [ :release_assets ])
@@ -21,8 +21,6 @@ module WarpEngine
@token = token || WarpEngine.config.woodpecker_api_token
end
# --- Repos ---
def list_repos
get("/api/repos")
end
@@ -39,8 +37,6 @@ module WarpEngine
delete("/api/repos/#{repo_id}")
end
# --- Secrets ---
def list_secrets(repo_id)
get("/api/repos/#{repo_id}/secrets")
end
@@ -59,8 +55,6 @@ module WarpEngine
delete("/api/repos/#{repo_id}/secrets/#{secret_name}")
end
# --- Pipelines ---
def list_pipelines(repo_id, page: 1, per_page: 25)
get("/api/repos/#{repo_id}/pipelines",
params: { page: page, perPage: per_page })
@@ -139,8 +133,7 @@ module WarpEngine
begin
JSON.parse(response.body)
rescue JSON::ParserError
# A wrong path falls through to the Woodpecker SPA, which answers
# 200 with index.html — surface that as an API error, not a parse one.
raise ApiError.new(
"Expected JSON from #{uri.path} but got: #{response.body.truncate(80)}",
status: response.code.to_i, body: response.body