myrr-rb 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 04e28d3ea019ce0843c0684022d231d322d388978188c9f798413d23b09643e0
4
+ data.tar.gz: 45f322be3df1e70288624705437ad0cbf3fcdcd2f8202df25f9273a44ae36705
5
+ SHA512:
6
+ metadata.gz: 3a0ba5768933e2d7fccd0dd221cabed252de5a51d5d89e3c3f094bc8e49fbddc61c3cd27db09cccbe65d6921fb34a1ec11db430cfa7c3a2603898d36ab38be49
7
+ data.tar.gz: db56d11806c24413d92c08b96f1c15d188dabfa078e61630129da50194e3a1fd3f5bea73e46c6250eab11aaeae6be4e625c981b153e799bc55181d16710247a7
data/CHANGELOG.md ADDED
@@ -0,0 +1,77 @@
1
+ ## [0.3.3] — 2026-06-02
2
+
3
+ ### Fixed
4
+
5
+ - **Front-matter deduplication.** The middleware no longer double-inserts
6
+ front-matter when inline front-matter is present in the body: the
7
+ Rails path strips any inline front-matter from the body and merges it
8
+ with the auto-detected metadata (inline wins per key), and the
9
+ non-Rails path reads inline front-matter straight from the body.
10
+
11
+ ## [0.3.2] — 2026-06-02
12
+
13
+ ### Added
14
+
15
+ - **Inline YAML front-matter in .md.erb views.** `Myrr::Port::Metadata`
16
+ now reads the current action's .md.erb template source for YAML
17
+ front-matter and merges it over auto-detected metadata — the view
18
+ wins for overlapping keys (title, description), auto-detection fills
19
+ in the rest. `token_count` remains middleware-computed only.
20
+
21
+ # Changelog
22
+
23
+ ## [0.3.1] — 2026-06-02
24
+
25
+ ### Fixed
26
+
27
+ - **Front-matter is always present.** Every markdown response now includes
28
+ YAML front-matter with at least `token_count`. Previously front-matter
29
+ was omitted when no title/description metadata was available. `token_count`
30
+ is a computed field the gem always knows — it should never be missing.
31
+
32
+ ## [0.3.0] — 2026-06-02
33
+
34
+ ### Added
35
+
36
+ - **YAML front-matter auto-injection.** When a page has metadata (title,
37
+ description), `Myrr::Port::Middleware` prepends standard YAML front-matter
38
+ to every markdown response. Agents get structured metadata inline.
39
+
40
+ - **`Myrr::Port::Metadata` concern.** Auto-included into every controller by
41
+ the railtie. Detects title/description from common Rails conventions:
42
+ `@myrr_title`, `@page_title`, `@title`, `content_for(:title)`, and
43
+ `Current.meta_tags`. Zero configuration.
44
+
45
+ - **Front-matter fields.** `title` and `description` are developer-overridable
46
+ via the detection chain. `token_count` is always computed from the final
47
+ body and cannot be overridden.
48
+
49
+ ### Changed
50
+
51
+ - **`Myrr::Port::Middleware`** now reads `env["myrr.metadata"]` and prepends
52
+ YAML front-matter wrapped in `---` delimiters when present. The token
53
+ count is recomputed after adding front-matter so `X-Token-Count` and the
54
+ body's `token_count` are always self-consistent.
55
+
56
+ ### Fixed
57
+
58
+ - **Metadata concern handles missing `Current`.** The detection chain
59
+ safely skips `Current.meta_tags` when the `Current` singleton isn't
60
+ defined (most Rails apps).
61
+
62
+ ## [0.2.0] — 2026-06-01
63
+
64
+ ### Added
65
+ - test/dummy/ Rails app — full integration test suite
66
+ - examples/rails/ — copyable Rails 8.1 example
67
+ - examples/rack/ — standalone Rack example
68
+ - CHANGELOG.md — all releases documented
69
+
70
+ ## [0.1.1] — 2026-06-01
71
+
72
+ ### Fixed
73
+ - X-Token-Count hardcoded, not configurable
74
+ - lib/myrr-rb.rb entry point for Bundler auto-require
75
+
76
+ ## [0.1.0] — 2026-06-01
77
+ Initial public release.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Myrr Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # Myrr — Agent-friendly content for your Rails app
2
+
3
+ **myrr-rb** is the Ruby/Rails client for [Myrr](https://myrrlabs.com), a protocol that makes web content natively accessible to AI agents.
4
+
5
+ ## Myrr Port
6
+
7
+ Myrr Port is the default, zero-configuration mode of myrr-rb. It adds agent-friendly markdown responses to your Rails app without a Go server, a database, or any configuration.
8
+
9
+ ### What it does
10
+
11
+ - Counts tokens on every `text/markdown` response
12
+ - Adds an `X-Token-Count` header so agents know the size of your content
13
+ - Works alongside normal HTML — browsers see HTML, agents see markdown
14
+
15
+ ### How it works
16
+
17
+ Rails 8.1 ships native markdown support: `format.md` in `respond_to`, `.md.erb` template resolution, and content negotiation via `Accept: text/markdown`, `.md` extension, or `?format=md`. Myrr Port defers entirely to Rails for this — we just count the tokens.
18
+
19
+ For pre-Rails-8.1 apps, the `text/markdown` MIME type is registered automatically.
20
+
21
+ ### Getting started
22
+
23
+ 1. Add the gem to your Gemfile:
24
+
25
+ ```ruby
26
+ gem "myrr-rb"
27
+ ```
28
+
29
+ 2. Run the installer:
30
+
31
+ ```bash
32
+ rails generate myrr:install
33
+ ```
34
+
35
+ 3. Create markdown views alongside your HTML views. For example, if you have `app/views/articles/show.html.erb`, create `app/views/articles/show.md.erb`:
36
+
37
+ ```markdown
38
+ # <%= @article.title %>
39
+
40
+ <%= @article.body %>
41
+ ```
42
+
43
+ 4. Add `format.md` to your controller's `respond_to` block:
44
+
45
+ ```ruby
46
+ def show
47
+ @article = Article.find(params[:id])
48
+ respond_to do |format|
49
+ format.html
50
+ format.md
51
+ end
52
+ end
53
+ ```
54
+
55
+ 5. An agent requesting with `Accept: text/markdown` now receives:
56
+
57
+ ```
58
+ Content-Type: text/markdown
59
+ X-Token-Count: 142
60
+ ```
61
+
62
+ No configuration needed. Everything works out of the box.
63
+
64
+ ## Development
65
+
66
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
67
+
68
+ ## License
69
+
70
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
@@ -0,0 +1,20 @@
1
+ require "rails/generators"
2
+
3
+ module Myrr
4
+ module Generators
5
+ # Rails generator that creates the myrr-rb initializer.
6
+ #
7
+ # @example
8
+ # rails generate myrr:install
9
+ class InstallGenerator < Rails::Generators::Base
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ desc "Creates a Myrr initializer for token counting on markdown responses."
13
+
14
+ # Create config/initializers/myrr.rb
15
+ def create_initializer
16
+ template "initializer.rb", "config/initializers/myrr.rb"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Myrr Port — Agent-friendly content for your Rails app
4
+ #
5
+ # Out of the box, myrr-rb counts tokens on every text/markdown response
6
+ # and adds an X-Token-Count header. No Go server, no database, no
7
+ # configuration required.
8
+ #
9
+ # For full documentation, see https://myrrlabs.com/docs
10
+
11
+ # No configuration is needed — everything works out of the box.
12
+ # This file exists so you know where to put future opt-in options.
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Myrr
8
+ module Bridge
9
+ Error = Class.new(::StandardError)
10
+ SellerNotApproved = Class.new(Error)
11
+ BudgetExceeded = Class.new(Error)
12
+ InsufficientFunds = Class.new(Error)
13
+ NoPaymentMethod = Class.new(Error)
14
+
15
+ # Resolve a 402 Payment Required by auto-paying and retrying.
16
+ #
17
+ # Calls the bridge endpoint on the agent's protocol server.
18
+ #
19
+ # @param agent_did [String] The agent's did:myrr: identifier
20
+ # @param seller_url [String] Base URL of the seller's protocol server
21
+ # @param seller_did [String] The seller's did:myrr: identifier
22
+ # @param payment_required [Hash] The 402 response body
23
+ # @param retry_method [String] HTTP method for the retry
24
+ # @param retry_path [String] Path to retry
25
+ # @param retry_headers [Hash, nil] Additional headers for the retry
26
+ # @return [Hash] The seller's response ({ "status_code" => 200, "body" => "..." })
27
+ # @raise [Myrr::Bridge::SellerNotApproved]
28
+ # @raise [Myrr::Bridge::BudgetExceeded]
29
+ # @raise [Myrr::Bridge::NoPaymentMethod]
30
+ def self.resolve(agent_did:, seller_url:, seller_did:, payment_required:, retry_method:, retry_path:, retry_headers: nil)
31
+ config = ::Myrr.config
32
+ server_url = config.protocol_server_url
33
+ api_key = config.site_api_key
34
+
35
+ uri = URI.join(server_url, "/v1/pay/resolve")
36
+ http = Net::HTTP.new(uri.host, uri.port)
37
+ http.open_timeout = config.open_timeout || 5
38
+ http.read_timeout = config.read_timeout || 30
39
+ http.use_ssl = (uri.scheme == "https")
40
+
41
+ payload = {
42
+ agent_did: agent_did,
43
+ seller_url: seller_url,
44
+ seller_did: seller_did,
45
+ payment_required: payment_required,
46
+ retry: {
47
+ method: retry_method,
48
+ path: retry_path,
49
+ headers: retry_headers || {}
50
+ }
51
+ }
52
+
53
+ request = Net::HTTP::Post.new(uri)
54
+ request["Content-Type"] = "application/json"
55
+ request["Authorization"] = "Bearer #{api_key}" if api_key
56
+ request.body = payload.to_json
57
+
58
+ response = http.request(request)
59
+ data = JSON.parse(response.body)
60
+
61
+ # Map HTTP status to bridge errors
62
+ case response.code.to_i
63
+ when 200..299
64
+ # Check for error payload in body (some bridge errors come as 200 with error field)
65
+ if data.is_a?(Hash)
66
+ case data["error"]
67
+ when "seller_not_approved"
68
+ raise SellerNotApproved, "Seller #{data["seller_did"]} is not approved"
69
+ when "budget_exceeded"
70
+ raise BudgetExceeded, "Budget exceeded: #{data["reason"]} (limit=#{data["limit"]}, current=#{data["current"]})"
71
+ when "no_payment_method"
72
+ raise NoPaymentMethod, data["message"]
73
+ end
74
+ end
75
+ data
76
+ when 400
77
+ if data["error"] == "no_payment_method"
78
+ raise NoPaymentMethod, data["message"]
79
+ end
80
+ raise Error, data["error"] || "Bad request"
81
+ when 402
82
+ raise InsufficientFunds, data["error"] || "Insufficient funds"
83
+ when 403
84
+ if data["error"] == "seller_not_approved"
85
+ raise SellerNotApproved, "Seller #{data["seller_did"]} is not approved"
86
+ elsif data["error"] == "budget_exceeded"
87
+ raise BudgetExceeded, "Budget exceeded: #{data["reason"]} (limit=#{data["limit"]}, current=#{data["current"]})"
88
+ end
89
+ raise Error, data["error"] || "Forbidden"
90
+ else
91
+ raise Error, data["error"] || "HTTP #{response.code}"
92
+ end
93
+ rescue JSON::ParserError
94
+ raise Error, "Invalid JSON response"
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Myrr
4
+ # Budget represents spending limit rules for an agent.
5
+ #
6
+ # @example
7
+ # Myrr::Budget.set(
8
+ # agent_did: "did:myrr:abc123",
9
+ # daily_max_cents: 100_00,
10
+ # monthly_max_cents: 500_00,
11
+ # allowed_merchant_categories: ["data_services", "api_services"]
12
+ # )
13
+ #
14
+ # budget = Myrr::Budget.for_agent("did:myrr:abc123")
15
+ class Budget
16
+ attr_reader :id, :agent_did, :per_transaction_max, :topup_per_transaction_max,
17
+ :daily_max, :monthly_max, :allowed_merchant_categories,
18
+ :created_at, :updated_at
19
+
20
+ # @param attrs [Hash] Budget attributes from the API
21
+ def initialize(attrs)
22
+ @id = attrs["id"]
23
+ @agent_did = attrs["agent_did"]
24
+ @per_transaction_max = attrs["per_transaction_max"]
25
+ @topup_per_transaction_max = attrs["topup_per_transaction_max"]
26
+ @daily_max = attrs["daily_max"]
27
+ @monthly_max = attrs["monthly_max"]
28
+ @allowed_merchant_categories = attrs["allowed_merchant_categories"]
29
+ @created_at = attrs["created_at"]
30
+ @updated_at = attrs["updated_at"]
31
+ end
32
+
33
+ # Create or update budget for an agent.
34
+ #
35
+ # @param agent_did [String] The did:myrr: identifier
36
+ # @param per_transaction_max_cents [Integer, nil] Max per transaction
37
+ # @param topup_per_transaction_max_cents [Integer, nil] Max per top-up
38
+ # @param daily_max_cents [Integer, nil] Max per day
39
+ # @param monthly_max_cents [Integer, nil] Max per month
40
+ # @param allowed_merchant_categories [Array<String>, nil] Allowed MCCs
41
+ # @return [Myrr::Budget]
42
+ def self.set(agent_did:, per_transaction_max_cents: nil, topup_per_transaction_max_cents: nil,
43
+ daily_max_cents: nil, monthly_max_cents: nil, allowed_merchant_categories: nil)
44
+ body = { agent_did: agent_did }
45
+ body[:per_transaction_max] = per_transaction_max_cents if per_transaction_max_cents
46
+ body[:topup_per_transaction_max] = topup_per_transaction_max_cents if topup_per_transaction_max_cents
47
+ body[:daily_max] = daily_max_cents if daily_max_cents
48
+ body[:monthly_max] = monthly_max_cents if monthly_max_cents
49
+ body[:allowed_merchant_categories] = allowed_merchant_categories if allowed_merchant_categories
50
+
51
+ resp = Myrr.client.send(:post, "/v1/budgets", body)
52
+ new(resp)
53
+ end
54
+
55
+ # Get budget for an agent.
56
+ #
57
+ # @param agent_did [String] The did:myrr: identifier
58
+ # @return [Myrr::Budget, nil] Returns nil if no budget has been set
59
+ def self.for_agent(agent_did)
60
+ resp = Myrr.client.send(:get, "/v1/budgets/agent/#{agent_did}")
61
+ new(resp)
62
+ rescue Myrr::Error => e
63
+ raise unless e.message.include?("agent_not_found")
64
+ nil
65
+ end
66
+ end
67
+ end
data/lib/myrr/card.rb ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Myrr
4
+ # Virtual card issued to a wallet.
5
+ #
6
+ # Card numbers, expiry, and CVC are returned ONLY at creation time.
7
+ # Subsequent lookups return last4 only.
8
+ class Card
9
+ attr_reader :id, :wallet_id, :last4, :brand, :status, :controls,
10
+ :number, :expiry, :cvc, :created_at, :updated_at
11
+
12
+ # @param attrs [Hash] Card attributes from the API
13
+ def initialize(attrs)
14
+ @id = attrs["id"]
15
+ @wallet_id = attrs["wallet_id"]
16
+ @last4 = attrs["last4"]
17
+ @brand = attrs["brand"] || "visa"
18
+ @status = attrs["status"]
19
+ @controls = attrs["controls"]
20
+ @number = attrs["number"]
21
+ @expiry = attrs["expiry"]
22
+ @cvc = attrs["cvc"]
23
+ @created_at = attrs["created_at"]
24
+ @updated_at = attrs["updated_at"]
25
+ end
26
+
27
+ # Freeze this card (no new authorizations).
28
+ def freeze!
29
+ resp = client.post("/v1/wallets/#{wallet_id}/cards/#{id}/freeze", {})
30
+ @status = resp["status"]
31
+ self
32
+ end
33
+
34
+ # Unfreeze this card.
35
+ def unfreeze!
36
+ resp = client.post("/v1/wallets/#{wallet_id}/cards/#{id}/unfreeze", {})
37
+ @status = resp["status"]
38
+ self
39
+ end
40
+
41
+ # Permanently cancel this card.
42
+ def cancel!
43
+ resp = client.delete("/v1/wallets/#{wallet_id}/cards/#{id}")
44
+ @status = resp["status"]
45
+ self
46
+ end
47
+
48
+ private
49
+
50
+ def client
51
+ Myrr.client
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,158 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ module Myrr
6
+ # Client for the Myrr protocol server API.
7
+ #
8
+ # Wraps the register, challenge, verify, revoke, and lookup endpoints.
9
+ # Also provides generic +post+, +get+, and +delete+ methods for resource clients.
10
+ #
11
+ # @example
12
+ # client = Myrr::Client.new(server_url: "http://localhost:8080", api_key: "sk-...")
13
+ # result = client.register(public_key: hex_pub_key)
14
+ # result = client.verify(did_key)
15
+ class Client
16
+ # @param server_url [String] Base URL of the protocol server
17
+ # @param api_key [String, nil] API key for authenticated operations
18
+ # @param open_timeout [Integer] HTTP open timeout in seconds
19
+ # @param read_timeout [Integer] HTTP read timeout in seconds
20
+ def initialize(server_url:, api_key: nil, open_timeout: 5, read_timeout: 10)
21
+ @server_url = server_url.chomp("/")
22
+ @api_key = api_key
23
+ @open_timeout = open_timeout
24
+ @read_timeout = read_timeout
25
+ end
26
+
27
+ # Register a new agent identity.
28
+ #
29
+ # @param public_key [String] Hex-encoded Ed25519 public key
30
+ # @param owner [Hash, nil] Optional owner info (+type+ and +value+)
31
+ # @return [Hash] Response with +id+, +did_key+, +status+, +created_at+
32
+ # @raise [Myrr::Error] On server error
33
+ def register(public_key:, owner: nil)
34
+ body = { public_key: public_key }
35
+ body[:owner] = owner if owner
36
+ post("/v1/identities/register", body)
37
+ end
38
+
39
+ # Get a challenge nonce for the given identity.
40
+ #
41
+ # @param did_key [String] The did:myrr: identifier
42
+ # @return [String] Challenge nonce
43
+ # @raise [Myrr::Error] On server error or if identity not found
44
+ def challenge(did_key)
45
+ resp = post("/v1/identities/challenge", { id: did_key })
46
+ resp["challenge"]
47
+ end
48
+
49
+ # Verify an agent's identity. Performs a status check via the protocol server.
50
+ #
51
+ # Returns a hash with +did_key+, +status+, and optional +owner+.
52
+ # Returns nil if the identity is not found.
53
+ #
54
+ # @param did_key [String] The did:myrr: identifier
55
+ # @return [Hash, nil] Agent info or nil
56
+ def verify(did_key)
57
+ challenge(did_key)
58
+ { "did_key" => did_key, "status" => "active" }
59
+ rescue Error => e
60
+ if e.message.include?("not_found") || e.message.include?("revoked")
61
+ nil
62
+ else
63
+ raise
64
+ end
65
+ end
66
+
67
+ # Revoke an agent identity.
68
+ #
69
+ # @param did_key [String] The did:myrr: identifier
70
+ # @return [Hash] Response with +status+, +did_key+, +revoked_at+
71
+ # @raise [Myrr::Error] On server error
72
+ def revoke(did_key)
73
+ post("/v1/identities/revoke", { id: did_key }, authenticated: true)
74
+ end
75
+
76
+ # Look up identities by owner.
77
+ #
78
+ # @param owner_type [String] Owner type (e.g., "email")
79
+ # @param owner_value [String] Owner value (e.g., "user@example.com")
80
+ # @return [Array<Hash>] List of matching identities
81
+ def lookup_by_owner(owner_type:, owner_value:)
82
+ get("/v1/identities", owner_type: owner_type, owner_value: owner_value)
83
+ end
84
+
85
+ # Send a POST request to the protocol server.
86
+ #
87
+ # @param path [String] API path (e.g., "/v1/wallets")
88
+ # @param body [Hash] Request body
89
+ # @param authenticated [Boolean] Whether to include the API key
90
+ # @return [Hash] Parsed JSON response
91
+ def post(path, body, authenticated: false)
92
+ uri = URI("#{@server_url}#{path}")
93
+ http = build_http(uri)
94
+
95
+ request = Net::HTTP::Post.new(uri)
96
+ request["Content-Type"] = "application/json"
97
+ request["Authorization"] = "Bearer #{@api_key}" if authenticated && @api_key
98
+ request.body = JSON.generate(body)
99
+
100
+ response = http.request(request)
101
+ handle_response(response)
102
+ end
103
+
104
+ # Send a GET request to the protocol server.
105
+ #
106
+ # @param path [String] API path (e.g., "/v1/wallets")
107
+ # @param params [Hash] Query parameters
108
+ # @return [Hash] Parsed JSON response
109
+ def get(path, **params)
110
+ uri = URI("#{@server_url}#{path}")
111
+ uri.query = URI.encode_www_form(params) if params.any?
112
+
113
+ http = build_http(uri)
114
+ request = Net::HTTP::Get.new(uri)
115
+
116
+ response = http.request(request)
117
+ handle_response(response)
118
+ end
119
+
120
+ # Send a DELETE request to the protocol server.
121
+ #
122
+ # @param path [String] API path
123
+ # @return [Hash] Parsed JSON response
124
+ def delete(path)
125
+ uri = URI("#{@server_url}#{path}")
126
+ http = build_http(uri)
127
+ request = Net::HTTP::Delete.new(uri)
128
+
129
+ response = http.request(request)
130
+ handle_response(response)
131
+ end
132
+
133
+ private
134
+
135
+ def build_http(uri)
136
+ http = Net::HTTP.new(uri.host, uri.port)
137
+ http.open_timeout = @open_timeout
138
+ http.read_timeout = @read_timeout
139
+ http.use_ssl = (uri.scheme == "https")
140
+ http
141
+ end
142
+
143
+ def handle_response(response)
144
+ case response
145
+ when Net::HTTPCreated, Net::HTTPOK
146
+ body = response.body
147
+ body && !body.empty? ? JSON.parse(body) : {}
148
+ when Net::HTTPNotFound
149
+ raise Error, "not_found: #{response.body}"
150
+ when Net::HTTPTooManyRequests
151
+ raise Error, "rate_limit_exceeded: #{response.body}"
152
+ else
153
+ error = JSON.parse(response.body) rescue { "error" => "unknown", "message" => response.body }
154
+ raise Error, "#{error["error"]}: #{error["message"]}"
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,58 @@
1
+ module Myrr
2
+ # Configuration for the myrr-rb gem.
3
+ #
4
+ # In its default (Port) state the gem requires zero configuration.
5
+ # Token counting on +text/markdown+ responses and the
6
+ # +X-Token-Count+ header are active out of the box.
7
+ #
8
+ # Additional attributes exist for the opt-in identity and pay layers
9
+ # (not auto-loaded — require explicitly).
10
+ class Configuration
11
+ # --- Identity-layer attributes (opt-in, not auto-loaded) ---
12
+
13
+ # URL of the Myrr protocol server (e.g. "http://localhost:8080").
14
+ # Required when using the identity or pay layers.
15
+ # @return [String, nil]
16
+ attr_accessor :protocol_server_url
17
+
18
+ # API key for authenticating with the protocol server.
19
+ # @return [String, nil]
20
+ attr_accessor :site_api_key
21
+
22
+ # Optional lambda that replaces protocol server calls for identity
23
+ # verification. Receives a +did:myrr:+ string, returns a hash with
24
+ # +did_key+, +status+, and optional +owner+, or nil.
25
+ # @return [Proc, nil]
26
+ attr_accessor :identity_adapter
27
+
28
+ # When true (default), requests proceed when identity verification
29
+ # fails (logging a warning). When false, returns 502 Bad Gateway.
30
+ # @return [Boolean]
31
+ attr_accessor :fail_open
32
+
33
+ # When true, the identity middleware auto-selects +:md+ format when
34
+ # an agent is detected and no explicit format is set.
35
+ # Default is false (port mode does not negotiate format).
36
+ # @return [Boolean]
37
+ attr_accessor :auto_format_md
38
+
39
+ # HTTP open timeout in seconds.
40
+ # @return [Integer]
41
+ attr_accessor :open_timeout
42
+
43
+ # HTTP read timeout in seconds.
44
+ # @return [Integer]
45
+ attr_accessor :read_timeout
46
+
47
+ def initialize
48
+ # Identity-layer defaults (nil / conservative — opt-in only)
49
+ @protocol_server_url = nil
50
+ @site_api_key = nil
51
+ @identity_adapter = nil
52
+ @fail_open = true
53
+ @auto_format_md = false
54
+ @open_timeout = 5
55
+ @read_timeout = 10
56
+ end
57
+ end
58
+ end