zerodrop 0.1.0 → 0.2.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 +4 -4
- data/README.md +21 -4
- data/lib/zerodrop/version.rb +1 -1
- data/lib/zerodrop.rb +58 -9
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f2142d8cff25793a9e3449ebf8bf47ebb553ef0bfc4a2e3cf4403798ccaa36e
|
|
4
|
+
data.tar.gz: 57cdc59a7a7d60554f7db1b9caecae6430f93cea49a1a979dc6c04daad4cf4a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a1e838f2ae4fb4aa72fe1030ccbf55aea3e5b95f3e0667bae811ac36d180065b7f4ef26de2c1ff3c7578f2b475315d6359f0b3b22f14309ef0a2754f2071eb74
|
|
7
|
+
data.tar.gz: f882b41e4ff02f0882d39af36af3793a6e327f04ed7510f1b7d86bd6059230739f1a8505b23e4a2fd7eb47edf3b557771230ad30d52cef5adcc2997c7f175563
|
data/README.md
CHANGED
|
@@ -126,6 +126,21 @@ visit email.magic_link
|
|
|
126
126
|
expect(page).to have_current_path("/dashboard")
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
## Free API key (private inboxes)
|
|
130
|
+
|
|
131
|
+
Without a key, anyone who knows an inbox's name can read it. With a free key, your inboxes start with your own prefix and only your key can read them.
|
|
132
|
+
|
|
133
|
+
1. Sign in with GitHub at [zerodrop.dev/keys](https://zerodrop.dev/keys) and create a key.
|
|
134
|
+
2. Set it as `ZERODROP_API_KEY`. The client reads it automatically, or you can pass `api_key:`.
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
mail = ZeroDrop::Client.new # reads ZERODROP_API_KEY when it's set
|
|
138
|
+
mail.generate_inbox
|
|
139
|
+
# => "k7x2-swift-x7k29ab@zerodrop-sandbox.online", where k7x2 is your prefix
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Reading a `k7x2-…` inbox without your key raises `ZeroDrop::AuthError` with the reason. `mail.prefix` holds your prefix if you build an address yourself. A key holding a character that can't be sent in an HTTP header (a line break, a byte-order mark) makes `ZeroDrop::Client.new` raise `ArgumentError`; the message never includes the key.
|
|
143
|
+
|
|
129
144
|
## Parallel test runs
|
|
130
145
|
|
|
131
146
|
`generate_inbox` runs locally — no network request, no collisions:
|
|
@@ -143,8 +158,8 @@ begin
|
|
|
143
158
|
email = mail.wait_for_latest(inbox, timeout: 15)
|
|
144
159
|
rescue ZeroDrop::TimeoutError
|
|
145
160
|
# No email arrived — check your app is sending correctly
|
|
146
|
-
rescue ZeroDrop::AuthError
|
|
147
|
-
#
|
|
161
|
+
rescue ZeroDrop::AuthError => e
|
|
162
|
+
# The inbox belongs to another key's prefix, or your key is wrong — e.message says which
|
|
148
163
|
rescue ZeroDrop::NetworkError => e
|
|
149
164
|
# Transport failure — e.message includes status page link
|
|
150
165
|
end
|
|
@@ -166,8 +181,9 @@ mail = ZeroDrop::Client.new(base_url: "https://your-instance.yourdomain.com")
|
|
|
166
181
|
|
|
167
182
|
| Method | Description |
|
|
168
183
|
|---|---|
|
|
169
|
-
| `Client.new(api_key: nil, base_url: ...)` | Create a client.
|
|
170
|
-
| `#generate_inbox` | Instant inbox address. No network request. |
|
|
184
|
+
| `Client.new(api_key: nil, base_url: ...)` | Create a client. `api_key` defaults to `ENV["ZERODROP_API_KEY"]`; without a key, inboxes are public to anyone who knows the name. |
|
|
185
|
+
| `#generate_inbox` | Instant inbox address. No network request. With a key, the name starts with your prefix. |
|
|
186
|
+
| `#prefix` | Your key's inbox prefix (`"k7x2"`), or `nil` without a key. |
|
|
171
187
|
| `#fetch_latest(inbox, filter: nil)` | Latest matching email or nil. |
|
|
172
188
|
| `#wait_for_latest(inbox, timeout: 10, poll_interval: 2, filter: nil)` | Block until email arrives. |
|
|
173
189
|
|
|
@@ -181,6 +197,7 @@ mail = ZeroDrop::Client.new(base_url: "https://your-instance.yourdomain.com")
|
|
|
181
197
|
| Email filtering | ✓ | ✓ |
|
|
182
198
|
| Email retention | 30 min | Extended |
|
|
183
199
|
| Custom domains | ✗ | ✓ |
|
|
200
|
+
| API key | ✓ free at [zerodrop.dev/keys](https://zerodrop.dev/keys) | ✓ |
|
|
184
201
|
|
|
185
202
|
Get a Workspace at [zerodrop.dev](https://zerodrop.dev)
|
|
186
203
|
|
data/lib/zerodrop/version.rb
CHANGED
data/lib/zerodrop.rb
CHANGED
|
@@ -22,6 +22,18 @@ module ZeroDrop
|
|
|
22
22
|
FREE_DOMAIN = "zerodrop-sandbox.online"
|
|
23
23
|
ADJECTIVES = %w[swift dark cold null void zero dead raw base core].freeze
|
|
24
24
|
|
|
25
|
+
# A free key from https://zerodrop.dev/keys: zd_<prefix>_<32 letters/digits>.
|
|
26
|
+
# Inboxes named <prefix>-... can be read only with that key.
|
|
27
|
+
KEY_FORMAT = /\Azd_([a-z0-9]{4})_[A-Za-z0-9]{32}\z/
|
|
28
|
+
|
|
29
|
+
# A key goes into an HTTP header, so after trimming it may hold only visible
|
|
30
|
+
# ASCII. Anything else is refused without echoing the key.
|
|
31
|
+
KEY_CHARS = /\A[\x21-\x7e]+\z/
|
|
32
|
+
|
|
33
|
+
# Whitespace around a key, including the no-break spaces and byte-order
|
|
34
|
+
# marks that copying and pasting can bring along.
|
|
35
|
+
KEY_PADDING = /\A[[:space:]\0\u{FEFF}]+|[[:space:]\0\u{FEFF}]+\z/
|
|
36
|
+
|
|
25
37
|
# Raised when no email arrives within the timeout.
|
|
26
38
|
class TimeoutError < StandardError
|
|
27
39
|
def initialize(inbox, timeout)
|
|
@@ -30,10 +42,11 @@ module ZeroDrop
|
|
|
30
42
|
end
|
|
31
43
|
end
|
|
32
44
|
|
|
33
|
-
# Raised
|
|
45
|
+
# Raised on a 401: the inbox belongs to an account's prefix and the request
|
|
46
|
+
# had no key or the wrong one. The message carries the server's reason.
|
|
34
47
|
class AuthError < StandardError
|
|
35
|
-
def initialize
|
|
36
|
-
super("invalid or missing API key")
|
|
48
|
+
def initialize(detail = nil)
|
|
49
|
+
super(detail || "invalid or missing API key")
|
|
37
50
|
end
|
|
38
51
|
end
|
|
39
52
|
|
|
@@ -74,21 +87,38 @@ module ZeroDrop
|
|
|
74
87
|
|
|
75
88
|
# The ZeroDrop API client.
|
|
76
89
|
class Client
|
|
77
|
-
#
|
|
90
|
+
# Your key's inbox prefix ("k7x2"), or nil without a key.
|
|
91
|
+
attr_reader :prefix
|
|
92
|
+
|
|
93
|
+
# @param api_key [String, nil] Free API key from https://zerodrop.dev/keys.
|
|
94
|
+
# Defaults to ENV["ZERODROP_API_KEY"]. Without a key, anyone who knows
|
|
95
|
+
# an inbox's name can read it.
|
|
78
96
|
# @param base_url [String] Override for self-hosted instances.
|
|
97
|
+
# @raise [ArgumentError] if the key holds a character that can't be sent in
|
|
98
|
+
# an HTTP header (a line break, a byte-order mark...)
|
|
79
99
|
def initialize(api_key: nil, base_url: DEFAULT_BASE_URL)
|
|
80
|
-
|
|
100
|
+
key = trim_key(api_key)
|
|
101
|
+
key = trim_key(ENV.fetch("ZERODROP_API_KEY", "")) if key.empty?
|
|
102
|
+
unless key.empty? || key.match?(KEY_CHARS)
|
|
103
|
+
raise ArgumentError, "ZeroDrop: the API key contains a character that can't be sent in an HTTP header, " \
|
|
104
|
+
"such as a line break or a byte-order mark. Copy it again from https://zerodrop.dev/keys."
|
|
105
|
+
end
|
|
106
|
+
@api_key = key.empty? ? nil : key
|
|
107
|
+
@prefix = KEY_FORMAT.match(@api_key.to_s)&.[](1)
|
|
81
108
|
@base_url = base_url.chomp("/")
|
|
82
109
|
end
|
|
83
110
|
|
|
84
111
|
# Returns a ready-to-use email address instantly.
|
|
85
|
-
# No network request is made.
|
|
112
|
+
# No network request is made. With a key, the name starts with your prefix.
|
|
86
113
|
#
|
|
87
|
-
# @return [String] e.g. "swift-x7k29ab@zerodrop-sandbox.online"
|
|
114
|
+
# @return [String] e.g. "swift-x7k29ab@zerodrop-sandbox.online",
|
|
115
|
+
# or "k7x2-swift-x7k29ab@zerodrop-sandbox.online" with the key zd_k7x2_...
|
|
88
116
|
def generate_inbox
|
|
89
117
|
adjective = ADJECTIVES.sample
|
|
90
118
|
suffix = SecureRandom.alphanumeric(7).downcase
|
|
91
|
-
"#{adjective}-#{suffix}
|
|
119
|
+
name = "#{adjective}-#{suffix}"
|
|
120
|
+
name = "#{@prefix}-#{name}" if @prefix
|
|
121
|
+
"#{name}@#{FREE_DOMAIN}"
|
|
92
122
|
end
|
|
93
123
|
|
|
94
124
|
# Returns the newest email matching the filter, or nil.
|
|
@@ -101,7 +131,7 @@ module ZeroDrop
|
|
|
101
131
|
uri = URI("#{@base_url}/api/inbox/#{name}?source=ruby-sdk")
|
|
102
132
|
|
|
103
133
|
res = http_get(uri)
|
|
104
|
-
raise AuthError if res.code == "401"
|
|
134
|
+
raise AuthError.new(error_detail(res)) if res.code == "401"
|
|
105
135
|
raise NetworkError, "API returned #{res.code}" unless res.code == "200"
|
|
106
136
|
|
|
107
137
|
payload = JSON.parse(res.body)
|
|
@@ -142,6 +172,16 @@ module ZeroDrop
|
|
|
142
172
|
|
|
143
173
|
private
|
|
144
174
|
|
|
175
|
+
# ENV strings can arrive as binary or US-ASCII (no UTF-8 locale), so the
|
|
176
|
+
# bytes are read as UTF-8 before trimming.
|
|
177
|
+
def trim_key(value)
|
|
178
|
+
text = value.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
179
|
+
return text.gsub(KEY_PADDING, "") if text.valid_encoding?
|
|
180
|
+
|
|
181
|
+
# Not UTF-8: a plain strip, and the character check refuses it.
|
|
182
|
+
value.to_s.b.strip
|
|
183
|
+
end
|
|
184
|
+
|
|
145
185
|
def http_get(uri)
|
|
146
186
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
147
187
|
http.use_ssl = uri.scheme == "https"
|
|
@@ -155,6 +195,15 @@ module ZeroDrop
|
|
|
155
195
|
http.request(req)
|
|
156
196
|
end
|
|
157
197
|
|
|
198
|
+
# The server explains a 401 in its JSON body; pass that on.
|
|
199
|
+
def error_detail(res)
|
|
200
|
+
body = JSON.parse(res.body.to_s)
|
|
201
|
+
detail = body["error"] if body.is_a?(Hash)
|
|
202
|
+
detail.is_a?(String) ? detail : nil
|
|
203
|
+
rescue JSON::ParserError, TypeError
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
158
207
|
def build_email(raw)
|
|
159
208
|
Email.new(
|
|
160
209
|
id: raw["id"],
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zerodrop
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ZeroDrop
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: ZeroDrop catches verification emails at Cloudflare's edge and auto-extracts
|
|
14
14
|
OTP codes and magic links. Test email verification, magic links, OTP and password
|