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:
@@ -10,6 +10,12 @@ ActiveAdmin.register WarpEngine::ApplicationToken, as: "Application Token" do
|
||||
scope :all, default: true
|
||||
scope("Active") { |scope| scope.where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
||||
scope("Expired") { |scope| scope.where("expires_at <= ?", Time.current) }
|
||||
# Two kinds of token share this table: one publishes software, the other reads the
|
||||
# catalog from somebody's desktop client. They are told apart by scope, and an admin
|
||||
# looking for one is rarely looking for the other.
|
||||
CATALOG_SCOPE_SQL = %(JSON_CONTAINS(COALESCE(scopes, '[]'), '"catalog"')).freeze
|
||||
scope("Publishing") { |scope| scope.where("NOT #{CATALOG_SCOPE_SQL}") }
|
||||
scope("Clients") { |scope| scope.where(CATALOG_SCOPE_SQL) }
|
||||
|
||||
index do
|
||||
id_column
|
||||
@@ -45,7 +51,7 @@ ActiveAdmin.register WarpEngine::ApplicationToken, as: "Application Token" do
|
||||
end
|
||||
f.input :name
|
||||
f.input :scopes_string, label: "Scopes (comma separated)",
|
||||
hint: %(The "update" scope is required for /build/publish, the "upload" scope for /build/upload.)
|
||||
hint: %(The "update" scope is required for /build/publish, the "upload" scope for /build/upload, the "catalog" scope for a client reading the API. Client tokens are normally issued by device sign-in rather than created here.)
|
||||
f.input :unrestricted, hint: "Internal token: exempt from owner isolation (enforce_software_ownership)."
|
||||
f.input :expires_at, hint: "Leave empty for a token that never expires."
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user