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
@@ -11,8 +11,7 @@ class CreateWarpEngineTables < ActiveRecord::Migration[8.0]
t.string :site
t.string :status, limit: 20, default: "development"
t.boolean :highlighted, default: false
# Owner of the publishing token (enforce_software_ownership) — no FK,
# the owner class comes from the host.
t.string :owner_type, limit: 128
t.bigint :owner_id
t.datetime :deleted_at, precision: 3
@@ -87,7 +86,7 @@ class CreateWarpEngineTables < ActiveRecord::Migration[8.0]
t.string :token_digest, limit: 64, null: false
t.string :token_prefix, limit: 12, null: false
t.json :scopes
# Internal token: exempt from enforce_software_ownership.
t.boolean :unrestricted, default: false, null: false
t.datetime :expires_at, precision: 3
t.datetime :last_used_at, precision: 3
@@ -98,12 +97,6 @@ class CreateWarpEngineTables < ActiveRecord::Migration[8.0]
t.index :deleted_at
end
# NOTE: device_grants is NOT created here. It ships as its own migration in the
# engine's db/migrate, which the host runs from the appended path — so creating it
# here as well would be a second CREATE TABLE for the same name. The same is true
# of application_tokens below, which predates this note and is why a host
# installing today has to delete that block from its copy by hand.
create_table :downloads do |t|
t.string :file_path, null: false
t.references :release, foreign_key: { on_delete: :nullify }, index: false
@@ -1,81 +1,4 @@
Rails.application.config.to_prepare do
WarpEngine.configure do |c|
# Storage locations for build artifacts and images (defaults from ENV:
# FILE_CONTAINER_PATH and IMAGE_CONTAINER_PATH).
# c.file_container_path = "/softwares"
# c.image_container_path = "/images"
# Shared secret of the /build/* endpoints (default: ENV["UPDATE_SECRET"]).
# With no secret configured the endpoints reject every request.
# c.update_secret = ENV["UPDATE_SECRET"]
# Auth source of the /build/* endpoints — an exclusive choice:
# :env — the shared secret above is accepted (default)
# :database — only DB-stored WarpEngine::ApplicationToken records are
# accepted (with the "update" scope); the shared secret
# stops working the moment you switch.
# :database mode also requires the owner class every token belongs to:
# c.application_token_source = :database
# c.application_token_owner_class = "AdminUser"
# Woodpecker configuration extension (/build/config): builder images of
# the served platforms and the CI server's signing key. An empty
# ci_platforms (default) keeps the feature inactive.
# c.ci_platforms = {
# "godot" => { builder: "registry.example/godot-builder:4.6" },
# "tic80" => { builder: "registry.example/tic80-builder:1.0",
# exporter: "registry.example/tic80pro:1.0" }
# }
# c.ci_extension_public_key_url = "https://ci.example.org/api/signature/public-key"
# c.ci_update_server = nil # nil: the request base_url
# Woodpecker CI management — repo sync, secret provisioning, pipeline control.
# All three must be set for the management features to activate.
# c.woodpecker_url = ENV["WOODPECKER_URL"] # e.g. "https://ci.example.org"
# c.woodpecker_api_token = ENV["WOODPECKER_API_TOKEN"] # Woodpecker PAT with admin access
# c.woodpecker_repo_owner = ENV["WOODPECKER_REPO_OWNER"] # forge org/user (e.g. "games")
# Size cap in bytes for /build/upload (and the admin file manager, default: 500MB).
# c.max_upload_size = 500 * 1024 * 1024
# Owner isolation: a DB token may only upload/publish its own owner's
# softwares (unrestricted tokens are exempt). Enable only after existing
# softwares got an owner (backfill)!
# c.enforce_software_ownership = true
# Who may see a title and who may download it. :open (the default) lists every
# software and serves every artifact — the behaviour of a catalog nobody sells
# from. A host that does sell supplies a policy answering three methods; see
# WarpEngine::AccessPolicy. What it returns is what clients are told, so a paid
# title can announce itself as paid instead of failing at the download.
# c.access_policy = MyStore::AccessPolicy.new
# Client sign-in. nil (default) means there is none: /api/auth/* is inactive and
# GET /api/service reports auth: null, so a client offers no sign-in at all. Set
# the class a *client* token belongs to — usually your user model — to turn on
# the device authorization grant.
# c.access_token_owner_class = "User"
#
# Your own page where a signed-in person types the code their client displayed.
# It calls WarpEngine::DeviceGrantService#approve. A path is made absolute
# against the request, so you need not know your own hostname (default "/devices").
# c.identity_verification_url = "/devices"
# A browser has a session rather than a bearer token, and the engine cannot
# read one. Say how, and your signed-in visitors are recognised on the
# read-only endpoints too:
# c.subject_resolver = ->(request) { request.env["warden"]&.user }
# c.device_code_ttl = 600 # seconds a pending code lives
# c.device_code_interval = 5 # seconds a client is told to wait between polls
# If host models also reference catalog images, register them so the
# admin Images page's orphan detection takes them into account:
# c.image_owners = [
# {
# label: "member",
# image_ids: -> { Member.where.not(image_id: nil).distinct.pluck(:image_id) },
# usage_label: ->(image) { "member" if Member.where(image_id: image.id).exists? }
# }
# ]
end
end
+1 -14
View File
@@ -1,7 +1,3 @@
# Az engine ActiveJob-ra épülő jobot szállít (PipelineSyncJob), ezért a
# framework betöltése a mi dolgunk: a host application.rb-je nem feltétlenül
# require-öli az active_job/railtie-t, és eager loadnál (production) a
# WarpEngine::ApplicationJob különben uninitialized constant-tal elszáll.
require "active_job/railtie"
require "blueprinter"
@@ -13,9 +9,7 @@ require "warp_engine/storage"
require "warp_engine/access"
module WarpEngine
# A tábláink prefix nélküliek (softwares, releases, ...) — az isolate_namespace
# által generált "warp_engine_" prefixet üresre cseréljük. Az engine.rb require-je
# előtt kell definiálva lennie.
def self.table_name_prefix
""
end
@@ -32,10 +26,6 @@ module WarpEngine
config.woodpecker_url.present? && config.woodpecker_api_token.present?
end
# A host innen tudja, hogy a publikálás ActiveSupport::Notifications-t szór
# ("warp_engine.publish"), és nem kell modell-callbackre kapaszkodnia.
# Régebbi engine-verziókon a metódus nem létezik, ezért a hívó oldalon
# respond_to?-val kérdezendő.
def self.instruments_publish?
true
end
@@ -44,13 +34,10 @@ module WarpEngine
Storage.adapter
end
# Who may see and download what. :open by default — see WarpEngine::AccessPolicy.
def self.access_policy
AccessPolicy.current
end
# The host has configured a subject class, so client sign-in is available. A client
# asks GET /api/service rather than this, but the engine's own controllers need it.
def self.identity_configured?
config.access_token_owner_class.present?
end
+3 -44
View File
@@ -1,29 +1,7 @@
module WarpEngine
# Who may see a title, and who may download it.
#
# Until now every catalog entry was public and every artifact was free: the API
# listed all software and /api/download handed over any file it could find. That is
# the right default for a catalog nobody sells from, and it stays the default — but a
# host that does sell needs the engine to *say so*, because the clients reading this
# API have no other way to learn it. A desktop client cannot know that a title is
# paid; it can only be told.
#
# This module is that seam. The default policy is byte for byte the previous
# behaviour, and a host swaps it on the configuration:
#
# c.access_policy = MyStorePolicy.new # or :open (default)
#
# A policy is any object answering to this contract:
#
# visible_software_scope(subject:) -> ActiveRecord::Relation
# access_for(software:, subject:) -> WarpEngine::Access
# authorize_download(asset:, subject:, request:) -> Access::Grant or nil
#
# `subject` is whoever the request authenticated as (see SubjectAuthentication), or
# nil for an anonymous caller. It is deliberately untyped here: the engine has no user
# model, and whose object this is belongs to the host.
module AccessPolicy
# Everything visible, everything open, no prices. The catalog as it always was.
class Open
def visible_software_scope(subject: nil)
WarpEngine::Software.all
@@ -33,8 +11,6 @@ module WarpEngine
Access::OPEN
end
# An open catalog authorises every asset it can find. Returning a bare grant
# rather than `true` keeps one return type across policies.
def authorize_download(asset: nil, subject: nil, request: nil)
Access::Grant::OPEN
end
@@ -56,24 +32,14 @@ module WarpEngine
@open_policy ||= Open.new
end
# Tests and hosts that swap the configuration at runtime.
def reset!
@open_policy = nil
end
end
end
# What a client is told about one title's availability.
#
# The vocabulary is deliberately generic — `gated`, `entitled`, `price` — because
# every client reading it serves more than one store. A word from any particular
# host's domain ("product", "purchase order", "library") would make the client
# that reads it specific to that host, which is exactly what this engine exists
# to prevent.
class Access
# Where a hosted (browser) build is played, when the host serves it somewhere other
# than the engine's own /file/ path. nil leaves the client with what it already
# builds, which is the pre-existing behaviour.
attr_reader :gated, :entitled, :price_cents, :currency, :purchase_url, :web_url
def initialize(gated: false, entitled: true, price_cents: nil, currency: nil,
@@ -86,9 +52,6 @@ module WarpEngine
@web_url = web_url
end
# An open catalog's answer, and the shape every response carries even when no
# policy is configured: a client should never have to tell "no access block" from
# "not gated". One of those is a question, the other is an answer.
OPEN = new.freeze
def as_json(*)
@@ -101,10 +64,6 @@ module WarpEngine
}
end
# A download the policy allowed.
#
# `filename` and `expires_in` let a host override what the engine would otherwise
# decide on its own; both nil means "you choose", which is what the open policy says.
class Grant
attr_reader :filename, :expires_in
+1 -51
View File
@@ -1,56 +1,6 @@
module WarpEngine
class Configuration
# Owner contract for image_owners elements:
# label: String
# image_ids: -> { Array<Integer> } — image ids used by the owner
# usage_label: ->(image) { String or nil } — label to display when in use
# application_token_source: auth source of the /build/* endpoints, exclusive.
# :env — the shared secret (update_secret) is accepted, DB tokens are not
# :database — only WarpEngine::ApplicationToken is accepted, the shared secret is not
# application_token_owner_class: class name of the mandatory token owner
# (e.g. "AdminUser"); nil makes :database mode reject every request.
# storage_adapter: where build artifacts are served from.
# :local (default) — the local filesystem under file_container_path,
# byte for byte the previous behaviour;
# any object — must answer file?/directory?/locate, see
# WarpEngine::Storage. Serving only: uploads and
# archive extraction still write to the local disk.
# max_upload_size: file size cap in bytes for /build/upload (and the admin file manager).
# enforce_software_ownership: when true, a DB token may only upload/publish
# its own owner's softwares (unrestricted tokens are exempt).
# Enable ONLY after the backfill: any token can claim an ownerless software.
# ci_platforms: platforms served by /build/config:
# { "godot" => { builder: "<image>" }, "tic80" => { builder: ..., exporter: ... } }
# Empty map = the feature is inactive (POST → 204, GET → 404).
# ci_extension_public_key(_url): the Woodpecker httpsig ed25519 public key as
# PEM, or a URL to fetch it from (e.g. https://ci.../api/signature/public-key).
# With neither set, POST /build/config rejects every request.
# ci_update_server: server URL written into the upload/publish steps; nil → the request's base_url.
# access_policy: who may see a title and who may download it.
# :open (default) — every software listed, every artifact served, no prices:
# byte for byte the previous behaviour;
# any object — must answer visible_software_scope/access_for/
# authorize_download, see WarpEngine::AccessPolicy.
# access_token_owner_class: class name of the subject a *client* token belongs to
# (e.g. "Accounts::User"). nil (default) means no client sign-in: /api/auth/* is
# inactive and GET /api/service reports auth: null. Deliberately separate from
# application_token_owner_class, which owns *publishing* tokens — a publisher and
# a customer are rarely the same kind of thing.
# identity_verification_url: the host's own page where a person approves a device
# code. Path or absolute URL; nil falls back to "/devices". The page is the host's
# because approving needs a session, a login and HTML — none of which is the
# engine's business.
# subject_resolver: how to recognise a caller that is not carrying a bearer token —
# a browser with a session, typically. A callable taking the Rack request and
# returning the host's own subject object, or nil:
# c.subject_resolver = ->(request) { request.env["warden"]&.user }
# nil (default) makes every non-bearer request anonymous, which is what the
# read-only API always did.
# device_code_ttl / device_code_interval: how long a pending device code lives, and
# how often a client is told to poll for it.
# woodpecker_url / woodpecker_api_token / woodpecker_repo_owner:
# Woodpecker CI management (repo sync, secret provisioning, pipeline control).
# All nil → the management features are inactive.
attr_accessor :file_container_path,
:image_container_path,
:update_secret,
+1 -16
View File
@@ -2,23 +2,10 @@ module WarpEngine
class Engine < ::Rails::Engine
isolate_namespace WarpEngine
# Az app/admin fájlok ActiveAdmin DSL-t tartalmaznak, nem definiálnak a
# fájlnévnek megfelelő konstansokat — a Zeitwerk (eager load) nem nyúlhat hozzájuk.
initializer "warp_engine.ignore_admin_dir", before: :setup_main_autoloader do
Rails.autoloaders.main.ignore(Engine.root.join("app/admin"))
end
# A jövőbeli katalógus-migrációk az engine db/migrate-jéből futnak a host
# rails db:migrate-jével, másolás nélkül.
#
# FONTOS: emiatt az engine és a host migrációi EGY névtérben vannak, és két
# azonos verziószám a host `db:migrate`-jét indulás előtt megállítja
# (DuplicateMigrationVersionError) — nem a miénket, hanem az övét, az egész
# alkalmazásban. Az engine migrációi ezért **valódi, másodperc-pontosságú
# időbélyeget** kapnak (20260819093412), soha nem kerek kézzel írt számot
# (20260819000001): pont az utóbbiakra ír rá egy host, ami ugyanaznap ugyanezzel
# a szokással ír migrációt. Így ütközött a device_grants a katalógus-API
# `carry_the_store_config_in_the_registry`-jével.
initializer "warp_engine.append_migrations" do |app|
unless app.root.to_s.start_with?(root.to_s)
config.paths["db/migrate"].expanded.each do |path|
@@ -27,13 +14,11 @@ module WarpEngine
end
end
# Az admin erőforrásokat a HOST ActiveAdmin példánya tölti be; az engine csak
# regisztrálja a saját app/admin könyvtárát. Auth/téma/route-ok a host dolga.
initializer "warp_engine.active_admin" do |app|
if defined?(ActiveAdmin)
admin_dir = Engine.root.join("app/admin").to_s
ActiveAdmin.application.load_paths << admin_dir
# dev módban az engine admin-fájljainak szerkesztése is triggerel reloadot
app.config.watchable_dirs[admin_dir] = [ :rb ]
end
end
+1 -27
View File
@@ -1,26 +1,5 @@
module WarpEngine
# Where the artifacts physically live.
#
# Until now every path in the engine was a local filesystem path. This module
# is the seam a host needs to serve builds from somewhere else (an object
# store behind a CDN, for example) without patching the engine.
#
# The default adapter is :local and behaves exactly as before - same paths,
# same traversal protection, same File.file? checks.
#
# A custom adapter is any object answering to this contract:
#
# file?(relative_path) -> true/false
# directory?(relative_path) -> true/false
# locate(relative_path, filename: nil, expires_in: nil) -> Location
#
# It is set on the configuration:
#
# c.storage_adapter = MyObjectStore.new # or :local (default)
#
# NOTE: ingestion (build/upload, archive extraction, the admin file manager)
# still writes to the local filesystem. A remote adapter therefore needs its
# own upload path today; the serving side is what this seam covers.
module Storage
Location = Struct.new(:kind, :path, :url, keyword_init: true) do
def file? = kind == :file
@@ -30,7 +9,6 @@ module WarpEngine
def self.redirect(url) = new(kind: :redirect, url: url)
end
# The local filesystem, rooted at config.file_container_path.
class LocalAdapter
def base_path
Pathname.new(WarpEngine.config.file_container_path)
@@ -50,15 +28,12 @@ module WarpEngine
File.directory?(path) && inside_base?(path)
end
# expires_in is part of the contract for signing adapters; the local
# filesystem has nothing to sign, so it is ignored here.
def locate(relative_path, filename: nil, expires_in: nil)
Location.file(absolute_path(relative_path).to_s)
end
private
# Path traversal guard: the resolved path must stay under the container.
def inside_base?(path)
root = base_path.realpath.to_s
Pathname.new(path).realpath.to_s.start_with?(root)
@@ -83,7 +58,6 @@ module WarpEngine
@local_adapter ||= LocalAdapter.new
end
# Tests and hosts that swap the configuration at runtime.
def reset!
@local_adapter = nil
end
-3
View File
@@ -1,8 +1,5 @@
module WarpEngine
VERSION = "0.5.2"
# The header every API response carries. Named here rather than written out at the one
# place that sets it: clients read it, the README documents it, and a string in three
# places is a string that eventually differs in one of them.
VERSION_HEADER = "WarpEngine-Version".freeze
end