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 +7 -0
- data/CHANGELOG.md +77 -0
- data/LICENSE +21 -0
- data/README.md +70 -0
- data/lib/generators/myrr/install/install_generator.rb +20 -0
- data/lib/generators/myrr/install/templates/initializer.rb +12 -0
- data/lib/myrr/bridge.rb +97 -0
- data/lib/myrr/budget.rb +67 -0
- data/lib/myrr/card.rb +54 -0
- data/lib/myrr/client.rb +158 -0
- data/lib/myrr/configuration.rb +58 -0
- data/lib/myrr/pay/client.rb +154 -0
- data/lib/myrr/pay/middleware.rb +186 -0
- data/lib/myrr/pay.rb +13 -0
- data/lib/myrr/port/metadata.rb +79 -0
- data/lib/myrr/port/middleware.rb +113 -0
- data/lib/myrr/rack/middleware.rb +93 -0
- data/lib/myrr/railtie.rb +25 -0
- data/lib/myrr/tokenizer.rb +28 -0
- data/lib/myrr/transaction.rb +57 -0
- data/lib/myrr/version.rb +3 -0
- data/lib/myrr/wallet.rb +131 -0
- data/lib/myrr-rb.rb +3 -0
- data/lib/myrr.rb +76 -0
- metadata +150 -0
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.
|
data/lib/myrr/bridge.rb
ADDED
|
@@ -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
|
data/lib/myrr/budget.rb
ADDED
|
@@ -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
|
data/lib/myrr/client.rb
ADDED
|
@@ -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
|