WarpEngine 0.5.0: a catalog that can say a title is not yours
A desktop client reading /api/software had no way to learn that a title costs money. There was nothing in the response to say so, no way to sign in, and no way to be told "you do not own this" — so a store with paid titles could only hand the client a 403 at download time and let it guess why. The fix belongs here rather than in the client. A client serves more than one store, so anything it knows about a particular one has to arrive from that store's own API; a rule compiled into the client is a rule that breaks every other catalog it reads. Three seams, each following the storage adapter's shape — documented contract, default that is byte for byte the old behaviour, one config key to replace it: - **access policy** — visible_software_scope / access_for / authorize_download. Every catalog entry now carries an `access` block (gated, entitled, price, purchaseUrl, webUrl) and both /api/download and /file/* ask before serving. The vocabulary is deliberately generic: a word from one host's domain would make every client that reads it specific to that host. - **client sign-in** — the device authorization grant (RFC 8628), over the host's own user model. The approval page stays the host's, because approving needs a session and HTML. Tokens are ApplicationTokens with a `catalog` scope, so publishing and reading stay separable. - **service descriptor** — GET /api/service says what this deployment is and whether it has a sign-in at all, which is how a client stops guessing. With no policy and no subject class configured — every deployment today — the API is unchanged: /api/auth/* answers 404, /api/service reports auth: null, and the 187 pre-existing examples pass untouched. A policy that raises is treated as a refusal, not permission. An artifact served because the gatekeeper crashed is the one failure mode this must not have, so a broken policy empties the catalog and denies the download. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,28 @@ class CreateWarpEngineTables < ActiveRecord::Migration[8.0]
|
||||
t.index :deleted_at
|
||||
end
|
||||
|
||||
# Device sign-in for clients that have no browser of their own (RFC 8628).
|
||||
# Short-lived rows: one exists for the minute or two between "the client asked"
|
||||
# and "the person answered". Only used where access_token_owner_class is set.
|
||||
create_table :device_grants do |t|
|
||||
t.string :device_code, limit: 64, null: false
|
||||
t.string :user_code, limit: 16, null: false
|
||||
t.string :client_name, limit: 128
|
||||
t.string :subject_type, limit: 128
|
||||
t.bigint :subject_id
|
||||
t.bigint :application_token_id
|
||||
# The issued token in the clear, cleared on the poll that hands it over — see
|
||||
# the model. Everything else about a token is stored as a digest.
|
||||
t.string :issued_token, limit: 64
|
||||
t.datetime :approved_at, precision: 3
|
||||
t.datetime :denied_at, precision: 3
|
||||
t.datetime :expires_at, precision: 3, null: false
|
||||
t.timestamps precision: 3, null: true
|
||||
t.index :device_code, unique: true
|
||||
t.index :user_code, unique: true
|
||||
t.index :expires_at
|
||||
end
|
||||
|
||||
create_table :downloads do |t|
|
||||
t.string :file_path, null: false
|
||||
t.references :release, foreign_key: { on_delete: :nullify }, index: false
|
||||
|
||||
@@ -43,6 +43,26 @@ Rails.application.config.to_prepare do
|
||||
# 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"
|
||||
# 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 = [
|
||||
|
||||
Reference in New Issue
Block a user