Class: Mpp::Server::Middleware

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/mpp/server/middleware.rb

Overview

Rack middleware that gates endpoints behind payment verification.

The pricing proc determines which requests require payment and at what price. It receives the Rack env and must return a charge options hash (with at least :amount) or nil for free endpoints. The pricing proc MUST NOT produce side effects.

Payment is verified BEFORE the downstream app runs — if verification fails, the app never executes and a 402 challenge is returned. Payment-Receipt is attached only when the app then returns 2xx, so a failed fulfillment is not reported as a successful paid response.

Example:

use Mpp::Server::Middleware,
  handler: my_handler,
  pricing: ->(env) { {amount: "1.00"} if env["PATH_INFO"] == "/paid" }

Defined Under Namespace

Classes: RackInputCapture

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, handler:, pricing:) ⇒ Middleware

Returns a new instance of Middleware.



28
29
30
31
32
# File 'lib/mpp/server/middleware.rb', line 28

def initialize(app, handler:, pricing:)
  @app = T.let(app, T.untyped)
  @handler = T.let(handler, T.untyped)
  @pricing = T.let(pricing, T.untyped)
end

Class Method Details

.mark_authorization_bound_response(headers, vary: ["Authorization"]) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/mpp/server/middleware.rb', line 78

def self.mark_authorization_bound_response(headers, vary: ["Authorization"])
  headers["Cache-Control"] = "no-store"

  vary_values = headers["Vary"].to_s.split(",").map do |value|
    value.strip.downcase
  end
  return if vary_values.include?("*")

  additions = vary.reject { |field| vary_values.include?(field.downcase) }
  return if additions.empty?

  headers["Vary"] = [headers["Vary"], *additions]
    .compact
    .reject(&:empty?)
    .join(", ")
end

Instance Method Details

#call(env) ⇒ Object



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/mpp/server/middleware.rb', line 35

def call(env)
  charge_opts = @pricing.call(env)
  return @app.call(env) unless charge_opts

  authorization = env["HTTP_AUTHORIZATION"]
  payment_signature = env["HTTP_PAYMENT_SIGNATURE"]
  accept_payment = env["HTTP_ACCEPT_PAYMENT"]
  http_method = env["REQUEST_METHOD"]
  url = request_url(env)
  body_capture = capture_request_body(env)

  request_body = body_capture&.materialize
  env["rack.input"] = StringIO.new(request_body || "") if body_capture

  result = verify_payment(
    env,
    charge_opts,
    authorization: authorization,
    payment_signature: payment_signature,
    accept_payment: accept_payment,
    http_method: http_method,
    url: url,
    request_body: request_body
  )

  challenge_response = challenge_rack_response(result, url: url, http_method: http_method)
  return challenge_response if challenge_response

  credential, receipt, extra_headers = paid_result(result)
  status, headers, body = @app.call(env)
  if success_status?(status)
    headers["Payment-Receipt"] = receipt.to_payment_receipt
    extra_headers.each { |key, value| headers[key] = value unless value.nil? }
    decorate_single_method_receipt(headers, credential, receipt, payment_signature)
  end
  vary = ["Authorization"]
  vary << "PAYMENT-SIGNATURE" if x402_bound?(headers, payment_signature)
  self.class.mark_authorization_bound_response(headers, vary: vary)

  [status, headers, body]
end