lib/tasks/oauth.rake
Rama main · réplica de lectura del repositorio original.
namespace :oauth do
# The registry (Oauth::FirstPartyClients): the personal access client, and
# each hosted MCP server whose resource FIZZY_OAUTH_MCP_RESOURCES names here.
# A secret is never minted or rotated; a client without one is named, for
# oauth:mint_client_secret. DRY_RUN=1 prints what each client would change
# and writes nothing.
desc "Create or converge Fizzy's first-party OAuth clients, all in one transaction (DRY_RUN=1 to print the changes and write nothing)"
task provision_first_party_clients: :environment do
if ActiveModel::Type::Boolean.new.cast(ENV["DRY_RUN"].presence)
puts "Dry run: nothing is written."
Oauth::FirstPartyClients.plan.each do |client|
changes = client.changes.except("client_id")
puts "#{client.client_id}: #{client.new_record? ? "would be created" : changes.any? ? "would converge" : "unchanged"}"
changes.each do |attribute, (from, to)|
from, to = [ from, to ].map { it && "[digest]" } if attribute == "client_secret_digest"
puts " #{attribute}: #{from.inspect} -> #{to.inspect}"
end
if client.invalid?
puts " #{client.client_id} wouldn't save: #{client.errors.full_messages.to_sentence}"
end
if Oauth::FirstPartyClients.awaiting_secret?(client)
puts " #{client.client_id} would have no secret yet, for oauth:mint_client_secret[#{client.client_id}]"
end
end
else
Oauth::FirstPartyClients.provision!.each do |client|
puts "#{client.client_id}: #{client.previously_new_record? ? "created" : "converged"}"
if Oauth::FirstPartyClients.awaiting_secret?(client)
puts " #{client.client_id} has no secret yet. Give it one with: bin/rails \"oauth:mint_client_secret[#{client.client_id}]\""
end
end
end
rescue ArgumentError => error
abort error.message
end
# For an MCP server the registry doesn't host, such as a self-hosted
# install's. Its client converges over the registry's baseline, as a
# hosted one does.
desc "Create or converge an MCP server's OAuth client outside the registry (RESOURCE=https://mcp.example.com/mcp, or the one FIZZY_OAUTH_MCP_RESOURCES names; CLIENT_ID=… for each resource past the first)"
task provision_mcp_client: :environment do
resource = ENV["RESOURCE"].presence || (Oauth.mcp_resources.one? ? Oauth.mcp_resources.first : nil)
if resource.nil?
abort "Name the MCP server's resource with RESOURCE=…, or configure exactly one in FIZZY_OAUTH_MCP_RESOURCES"
elsif !Oauth.mcp_resource?(resource)
abort "#{resource} isn't in FIZZY_OAUTH_MCP_RESOURCES, so its client couldn't exchange tokens"
else
client = Oauth::FirstPartyClients.provision_mcp_server!(resource, client_id: ENV["CLIENT_ID"].presence)
puts "#{client.client_id} fronts #{client.protected_resource}. Give it a secret with: bin/rails \"oauth:mint_client_secret[#{client.client_id}]\""
end
rescue ArgumentError => error
abort error.message
end
desc "Mint a new secret for an operator-provisioned confidential client, printed once; the old one stops working"
task :mint_client_secret, [ :client_id ] => :environment do |_task, args|
client = Oauth::Client.find_by!(client_id: args.fetch(:client_id))
secret = client.rotate_secret!
puts "Secret for #{client.client_id} (shown once, store it now):"
puts secret
end
end