zerodrop 0.1.0
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/LICENSE +21 -0
- data/README.md +189 -0
- data/lib/zerodrop/version.rb +5 -0
- data/lib/zerodrop.rb +197 -0
- metadata +55 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dce1e4e518096ffccfd4fdc04f743750b64230b58cd11e631b49ee116b1ad143
|
|
4
|
+
data.tar.gz: a847df3318ff736eb8500a1407b1e2d602c637f54e18dfb0959db2d1d786fc0c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a8bfc595cd95a7b75ed9c652c27ccea75353e25b7c4355d6512c514a000773eedc09749e107738d1820c6e71cef87f619a2f41b06ebeb9f253f42798ea555eae
|
|
7
|
+
data.tar.gz: 0fbf9f95d8772a227f9a2b46c2b9092238b9dbdb75f0ddbc8a835df83bd77f28cde57c6b9b966a66b2917da534b7b5d7382217e755eaff4ba7227edc02c2c8b3
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ZeroDrop
|
|
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,189 @@
|
|
|
1
|
+
# zerodrop
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/zerodrop)
|
|
4
|
+
[](https://github.com/zerodrop-dev/zerodrop-ruby/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Email verification infrastructure for CI pipelines.
|
|
8
|
+
|
|
9
|
+
Send a verification email. Catch it at the edge. Get `email.otp` and `email.magic_link` back — auto-extracted, no regex, no Docker, no signup.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
email = mail.wait_for_latest(inbox, timeout: 15)
|
|
13
|
+
|
|
14
|
+
email.otp # => "123456" — auto-extracted
|
|
15
|
+
email.magic_link # => "https://..." — no regex needed
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**[Documentation](https://docs.zerodrop.dev)** · [GitHub](https://github.com/zerodrop-dev) · [Status](https://zerodrop.instatus.com)
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
gem install zerodrop
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or in your Gemfile:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
gem "zerodrop", group: :test
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Zero runtime dependencies. Ruby 3.0+.
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
require "zerodrop"
|
|
38
|
+
|
|
39
|
+
mail = ZeroDrop::Client.new
|
|
40
|
+
inbox = mail.generate_inbox
|
|
41
|
+
# => "swift-x7k29ab@zerodrop-sandbox.online"
|
|
42
|
+
|
|
43
|
+
# Trigger your app's email flow with the inbox address...
|
|
44
|
+
|
|
45
|
+
email = mail.wait_for_latest(inbox, timeout: 15)
|
|
46
|
+
|
|
47
|
+
email.subject # => "Verify your email"
|
|
48
|
+
email.otp # => "123456" — auto-extracted, no regex
|
|
49
|
+
email.magic_link # => "https://..." — auto-extracted
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## RSpec + Capybara
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
RSpec.describe "Signup email verification", type: :system do
|
|
56
|
+
let(:mail) { ZeroDrop::Client.new }
|
|
57
|
+
|
|
58
|
+
it "verifies the account via emailed OTP" do
|
|
59
|
+
inbox = mail.generate_inbox
|
|
60
|
+
|
|
61
|
+
visit "/signup"
|
|
62
|
+
fill_in "Email", with: inbox
|
|
63
|
+
fill_in "Password", with: "TestPassword123!"
|
|
64
|
+
click_button "Create account"
|
|
65
|
+
|
|
66
|
+
email = mail.wait_for_latest(inbox, timeout: 15)
|
|
67
|
+
|
|
68
|
+
expect(email.otp).not_to be_nil
|
|
69
|
+
fill_in "Code", with: email.otp
|
|
70
|
+
click_button "Verify"
|
|
71
|
+
|
|
72
|
+
expect(page).to have_current_path("/dashboard")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Minitest
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
require "minitest/autorun"
|
|
81
|
+
require "zerodrop"
|
|
82
|
+
|
|
83
|
+
class SignupTest < Minitest::Test
|
|
84
|
+
def test_email_verification
|
|
85
|
+
mail = ZeroDrop::Client.new
|
|
86
|
+
inbox = mail.generate_inbox
|
|
87
|
+
|
|
88
|
+
# Trigger signup with inbox...
|
|
89
|
+
|
|
90
|
+
email = mail.wait_for_latest(inbox, timeout: 15)
|
|
91
|
+
|
|
92
|
+
refute_nil email.otp
|
|
93
|
+
assert_match(/\A\d{6}\z/, email.otp)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Email filtering
|
|
99
|
+
|
|
100
|
+
Filter by sender, subject, body, or extracted fields:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
email = mail.wait_for_latest(
|
|
104
|
+
inbox,
|
|
105
|
+
timeout: 15,
|
|
106
|
+
filter: ZeroDrop::Filter.new(
|
|
107
|
+
from: "noreply@yourapp.com",
|
|
108
|
+
subject: "Verify",
|
|
109
|
+
has_otp: true
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
All string filters are case-insensitive partial matches.
|
|
115
|
+
|
|
116
|
+
## Magic link flows
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
email = mail.wait_for_latest(
|
|
120
|
+
inbox,
|
|
121
|
+
timeout: 15,
|
|
122
|
+
filter: ZeroDrop::Filter.new(has_magic_link: true)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
visit email.magic_link
|
|
126
|
+
expect(page).to have_current_path("/dashboard")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Parallel test runs
|
|
130
|
+
|
|
131
|
+
`generate_inbox` runs locally — no network request, no collisions:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
# parallel_tests, flatware, turbo_tests — all safe
|
|
135
|
+
# Each process gets isolated inboxes automatically
|
|
136
|
+
inbox = mail.generate_inbox # unique every call
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Error handling
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
begin
|
|
143
|
+
email = mail.wait_for_latest(inbox, timeout: 15)
|
|
144
|
+
rescue ZeroDrop::TimeoutError
|
|
145
|
+
# No email arrived — check your app is sending correctly
|
|
146
|
+
rescue ZeroDrop::AuthError
|
|
147
|
+
# Invalid API key
|
|
148
|
+
rescue ZeroDrop::NetworkError => e
|
|
149
|
+
# Transport failure — e.message includes status page link
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Workspaces
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
mail = ZeroDrop::Client.new(api_key: ENV["ZERODROP_API_KEY"])
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Self-hosted
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
mail = ZeroDrop::Client.new(base_url: "https://your-instance.yourdomain.com")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## API
|
|
166
|
+
|
|
167
|
+
| Method | Description |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `Client.new(api_key: nil, base_url: ...)` | Create a client. No args = free sandbox mode. |
|
|
170
|
+
| `#generate_inbox` | Instant inbox address. No network request. |
|
|
171
|
+
| `#fetch_latest(inbox, filter: nil)` | Latest matching email or nil. |
|
|
172
|
+
| `#wait_for_latest(inbox, timeout: 10, poll_interval: 2, filter: nil)` | Block until email arrives. |
|
|
173
|
+
|
|
174
|
+
## Free vs Workspace
|
|
175
|
+
|
|
176
|
+
| | Free | Workspace |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| Inbox generation | ✓ | ✓ |
|
|
179
|
+
| OTP auto-extraction | ✓ | ✓ |
|
|
180
|
+
| Magic link extraction | ✓ | ✓ |
|
|
181
|
+
| Email filtering | ✓ | ✓ |
|
|
182
|
+
| Email retention | 30 min | Extended |
|
|
183
|
+
| Custom domains | ✗ | ✓ |
|
|
184
|
+
|
|
185
|
+
Get a Workspace at [zerodrop.dev](https://zerodrop.dev)
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
data/lib/zerodrop.rb
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "time"
|
|
8
|
+
|
|
9
|
+
require_relative "zerodrop/version"
|
|
10
|
+
|
|
11
|
+
# ZeroDrop — disposable email inboxes for testing auth flows in CI.
|
|
12
|
+
# OTPs and magic links auto-extracted at Cloudflare's edge.
|
|
13
|
+
# No regex, no Docker, no signup.
|
|
14
|
+
#
|
|
15
|
+
# mail = ZeroDrop::Client.new
|
|
16
|
+
# inbox = mail.generate_inbox
|
|
17
|
+
# email = mail.wait_for_latest(inbox, timeout: 15)
|
|
18
|
+
# email.otp # => "847291"
|
|
19
|
+
# email.magic_link # => "https://..."
|
|
20
|
+
module ZeroDrop
|
|
21
|
+
DEFAULT_BASE_URL = "https://zerodrop.dev"
|
|
22
|
+
FREE_DOMAIN = "zerodrop-sandbox.online"
|
|
23
|
+
ADJECTIVES = %w[swift dark cold null void zero dead raw base core].freeze
|
|
24
|
+
|
|
25
|
+
# Raised when no email arrives within the timeout.
|
|
26
|
+
class TimeoutError < StandardError
|
|
27
|
+
def initialize(inbox, timeout)
|
|
28
|
+
super("no email received at #{inbox.inspect} within #{timeout}s — " \
|
|
29
|
+
"check that your app is sending to the correct address")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Raised when an invalid API key is provided.
|
|
34
|
+
class AuthError < StandardError
|
|
35
|
+
def initialize
|
|
36
|
+
super("invalid or missing API key")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Raised on transport-level failures.
|
|
41
|
+
class NetworkError < StandardError; end
|
|
42
|
+
|
|
43
|
+
# A caught email with auto-extracted fields.
|
|
44
|
+
Email = Struct.new(
|
|
45
|
+
:id, :from, :to, :subject, :body, :raw_body, :received_at,
|
|
46
|
+
:otp, :magic_link,
|
|
47
|
+
keyword_init: true
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Filter for narrowing which email is returned.
|
|
51
|
+
# All string matches are case-insensitive partial matches.
|
|
52
|
+
Filter = Struct.new(
|
|
53
|
+
:from, :subject, :body, :has_otp, :has_magic_link,
|
|
54
|
+
keyword_init: true
|
|
55
|
+
) do
|
|
56
|
+
def matches?(email)
|
|
57
|
+
return false if from && !email.from.to_s.downcase.include?(from.downcase)
|
|
58
|
+
return false if subject && !email.subject.to_s.downcase.include?(subject.downcase)
|
|
59
|
+
return false if body && !email.body.to_s.downcase.include?(body.downcase)
|
|
60
|
+
|
|
61
|
+
unless has_otp.nil?
|
|
62
|
+
return false if has_otp && email.otp.to_s.empty?
|
|
63
|
+
return false if !has_otp && !email.otp.to_s.empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
unless has_magic_link.nil?
|
|
67
|
+
return false if has_magic_link && email.magic_link.to_s.empty?
|
|
68
|
+
return false if !has_magic_link && !email.magic_link.to_s.empty?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The ZeroDrop API client.
|
|
76
|
+
class Client
|
|
77
|
+
# @param api_key [String, nil] Workspace API key. Omit for free sandbox mode.
|
|
78
|
+
# @param base_url [String] Override for self-hosted instances.
|
|
79
|
+
def initialize(api_key: nil, base_url: DEFAULT_BASE_URL)
|
|
80
|
+
@api_key = api_key
|
|
81
|
+
@base_url = base_url.chomp("/")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns a ready-to-use email address instantly.
|
|
85
|
+
# No network request is made.
|
|
86
|
+
#
|
|
87
|
+
# @return [String] e.g. "swift-x7k29ab@zerodrop-sandbox.online"
|
|
88
|
+
def generate_inbox
|
|
89
|
+
adjective = ADJECTIVES.sample
|
|
90
|
+
suffix = SecureRandom.alphanumeric(7).downcase
|
|
91
|
+
"#{adjective}-#{suffix}@#{FREE_DOMAIN}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns the newest email matching the filter, or nil.
|
|
95
|
+
#
|
|
96
|
+
# @param inbox [String] inbox address or name
|
|
97
|
+
# @param filter [ZeroDrop::Filter, nil]
|
|
98
|
+
# @return [ZeroDrop::Email, nil]
|
|
99
|
+
def fetch_latest(inbox, filter: nil)
|
|
100
|
+
name = inbox_name(inbox)
|
|
101
|
+
uri = URI("#{@base_url}/api/inbox/#{name}?source=ruby-sdk")
|
|
102
|
+
|
|
103
|
+
res = http_get(uri)
|
|
104
|
+
raise AuthError if res.code == "401"
|
|
105
|
+
raise NetworkError, "API returned #{res.code}" unless res.code == "200"
|
|
106
|
+
|
|
107
|
+
payload = JSON.parse(res.body)
|
|
108
|
+
emails = payload.fetch("emails", [])
|
|
109
|
+
|
|
110
|
+
emails.each do |raw|
|
|
111
|
+
email = build_email(raw)
|
|
112
|
+
return email if filter.nil? || filter.matches?(email)
|
|
113
|
+
end
|
|
114
|
+
nil
|
|
115
|
+
rescue JSON::ParserError => e
|
|
116
|
+
raise NetworkError, "invalid response: #{e.message}"
|
|
117
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => e
|
|
118
|
+
raise NetworkError, "#{e.message} (check https://zerodrop.instatus.com)"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Blocks until an email matching the filter arrives.
|
|
122
|
+
# Polls every `poll_interval` seconds up to `timeout` seconds.
|
|
123
|
+
#
|
|
124
|
+
# @param inbox [String] inbox address or name
|
|
125
|
+
# @param timeout [Numeric] seconds to wait (default 10)
|
|
126
|
+
# @param poll_interval [Numeric] seconds between polls (default 2)
|
|
127
|
+
# @param filter [ZeroDrop::Filter, nil]
|
|
128
|
+
# @return [ZeroDrop::Email]
|
|
129
|
+
# @raise [ZeroDrop::TimeoutError] if nothing arrives in time
|
|
130
|
+
def wait_for_latest(inbox, timeout: 10, poll_interval: 2, filter: nil)
|
|
131
|
+
deadline = Time.now + timeout
|
|
132
|
+
|
|
133
|
+
loop do
|
|
134
|
+
email = fetch_latest(inbox, filter: filter)
|
|
135
|
+
return email if email
|
|
136
|
+
|
|
137
|
+
raise TimeoutError.new(inbox, timeout) if Time.now + poll_interval > deadline
|
|
138
|
+
|
|
139
|
+
sleep poll_interval
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def http_get(uri)
|
|
146
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
147
|
+
http.use_ssl = uri.scheme == "https"
|
|
148
|
+
http.open_timeout = 10
|
|
149
|
+
http.read_timeout = 30
|
|
150
|
+
|
|
151
|
+
req = Net::HTTP::Get.new(uri)
|
|
152
|
+
req["Authorization"] = "Bearer #{@api_key}" if @api_key
|
|
153
|
+
req["User-Agent"] = "zerodrop-ruby/#{VERSION}"
|
|
154
|
+
|
|
155
|
+
http.request(req)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def build_email(raw)
|
|
159
|
+
Email.new(
|
|
160
|
+
id: raw["id"],
|
|
161
|
+
from: raw["from"],
|
|
162
|
+
to: raw["to"],
|
|
163
|
+
subject: raw["subject"],
|
|
164
|
+
body: extract_body(raw["raw"]),
|
|
165
|
+
raw_body: raw["raw"],
|
|
166
|
+
received_at: parse_time(raw["receivedAt"]),
|
|
167
|
+
otp: raw["otp"].to_s.empty? ? nil : raw["otp"],
|
|
168
|
+
magic_link: raw["magicLink"].to_s.empty? ? nil : raw["magicLink"]
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def parse_time(str)
|
|
173
|
+
Time.parse(str) if str
|
|
174
|
+
rescue ArgumentError, TypeError
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def extract_body(raw)
|
|
179
|
+
return "" if raw.nil? || raw.empty?
|
|
180
|
+
|
|
181
|
+
if (m = raw.match(%r{Content-Type: text/plain[^\r\n]*\r\n\r\n(.*?)(?:\r\n--|\r\n\r\n--)}m))
|
|
182
|
+
return m[1].strip
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
parts = raw.split("\r\n\r\n", 2)
|
|
186
|
+
return "" unless parts.length == 2
|
|
187
|
+
|
|
188
|
+
body = parts[1].strip
|
|
189
|
+
body.length > 5000 ? body[0, 5000] : body
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def inbox_name(inbox)
|
|
193
|
+
name = inbox.include?("@") ? inbox.split("@").first : inbox
|
|
194
|
+
name.downcase
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: zerodrop
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ZeroDrop
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: ZeroDrop catches verification emails at Cloudflare's edge and auto-extracts
|
|
14
|
+
OTP codes and magic links. Test email verification, magic links, OTP and password
|
|
15
|
+
reset flows in RSpec, Minitest or Capybara — no regex, no Docker, no signup.
|
|
16
|
+
email:
|
|
17
|
+
- founder@zerodrop.dev
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/zerodrop.rb
|
|
25
|
+
- lib/zerodrop/version.rb
|
|
26
|
+
homepage: https://zerodrop.dev
|
|
27
|
+
licenses:
|
|
28
|
+
- MIT
|
|
29
|
+
metadata:
|
|
30
|
+
homepage_uri: https://zerodrop.dev
|
|
31
|
+
source_code_uri: https://github.com/zerodrop-dev/zerodrop-ruby
|
|
32
|
+
documentation_uri: https://docs.zerodrop.dev
|
|
33
|
+
bug_tracker_uri: https://github.com/zerodrop-dev/zerodrop-ruby/issues
|
|
34
|
+
rubygems_mfa_required: 'true'
|
|
35
|
+
post_install_message:
|
|
36
|
+
rdoc_options: []
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '3.0'
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubygems_version: 3.4.19
|
|
51
|
+
signing_key:
|
|
52
|
+
specification_version: 4
|
|
53
|
+
summary: Disposable email inboxes for testing auth flows in CI — OTPs and magic links
|
|
54
|
+
auto-extracted.
|
|
55
|
+
test_files: []
|