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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user