hook0-client 2.0.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/README.md +235 -0
- data/assets/ruby-flow.svg +153 -0
- data/lib/hook0/client.rb +691 -0
- data/lib/hook0/errors.rb +78 -0
- data/lib/hook0/generated/all.rb +7 -0
- data/lib/hook0/generated/api.rb +1097 -0
- data/lib/hook0/generated/errors.rb +235 -0
- data/lib/hook0/generated/models.rb +3093 -0
- data/lib/hook0/runtime.rb +299 -0
- data/lib/hook0/signature.rb +294 -0
- data/lib/hook0/transport.rb +359 -0
- data/lib/hook0/version.rb +6 -0
- data/lib/hook0.rb +23 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 35bdd9ee3b63265c0c6f99c16d51bd101384b173e1a6e9d53bd267ae3ae7f312
|
|
4
|
+
data.tar.gz: 7b02d005f60be738140de312e2896ed36d2c33bab04032204211d08b77456f02
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aeb9643df482d368a4d62c10a993e2cbe91a4c592e127acc92146e6a2c9d86778ea55c7f236c220807b4eec1d1a3cd19ab0f3f145f73521c6895421968438830
|
|
7
|
+
data.tar.gz: a8e633e190a2499c19606d592a6e6c3f1ec1bb774d4ef3f0a71db4eae72c23b5373174d58e91142da9b24ceb5cf482cf7fb972cd3a72e44da00664bde9528929
|
data/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Hook0 Ruby SDK
|
|
4
|
+
|
|
5
|
+
**Send and verify webhooks with nothing but the standard library**
|
|
6
|
+
|
|
7
|
+
<br/>
|
|
8
|
+
|
|
9
|
+
<img src="assets/ruby-flow.svg" alt="How the Hook0 Ruby SDK sits between your application and your users" width="850"/>
|
|
10
|
+
|
|
11
|
+
<br/>
|
|
12
|
+
<br/>
|
|
13
|
+
|
|
14
|
+
[](https://rubygems.org/gems/hook0-client)
|
|
15
|
+
[](./LICENSE)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What is this?
|
|
22
|
+
|
|
23
|
+
The Ruby SDK for [Hook0](https://www.hook0.com/), the open source Webhooks-as-a-Service platform
|
|
24
|
+
for SaaS applications. It sends events, declares the event types your application uses, verifies the
|
|
25
|
+
signature of a webhook you receive, and calls every operation the API declares through generated,
|
|
26
|
+
documented types.
|
|
27
|
+
|
|
28
|
+
It reaches the network, verifies signatures and decodes what the API answers with the standard
|
|
29
|
+
library alone: `net/http` for requests, `openssl` for signatures, `json` for documents,
|
|
30
|
+
`securerandom` for identifiers. Installing it never drags a transitive dependency into an
|
|
31
|
+
application that only wanted to send an event.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Send events** - under an ID the client mints, so a retry cannot duplicate one
|
|
36
|
+
- **Declare event types** - upsert the ones your application emits, in one call
|
|
37
|
+
- **Verify signatures** - HMAC-SHA256 over a bilateral clock window
|
|
38
|
+
- **The whole API, typed** - one class per schema, one exception per problem, one method per operation
|
|
39
|
+
- **Bounded everywhere** - attempts, backoff, timeouts, payload and answer, all yours to set
|
|
40
|
+
- **Zero dependencies** - the standard library and nothing else
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
### 1. Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
gem install hook0-client
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or add `gem "hook0-client"` to your Gemfile. Ruby 3.1 or later.
|
|
53
|
+
|
|
54
|
+
### 2. Send an event
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
require "hook0"
|
|
58
|
+
|
|
59
|
+
client = Hook0::Client.new(
|
|
60
|
+
"https://app.hook0.com/api/v1",
|
|
61
|
+
application_id,
|
|
62
|
+
token
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
event_id = client.send_event(
|
|
66
|
+
Hook0::Event.new(
|
|
67
|
+
event_type: "billing.invoice.paid",
|
|
68
|
+
payload: '{"invoice": "in_123"}',
|
|
69
|
+
payload_content_type: "application/json",
|
|
70
|
+
labels: { "environment" => "production" }
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Verify a webhook you receive
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
begin
|
|
79
|
+
Hook0.verify_webhook_signature(
|
|
80
|
+
request.headers["X-Hook0-Signature"],
|
|
81
|
+
request.body,
|
|
82
|
+
request.headers,
|
|
83
|
+
subscription_secret,
|
|
84
|
+
300
|
|
85
|
+
)
|
|
86
|
+
rescue Hook0::ClientError
|
|
87
|
+
# answer 400, and do not act on the delivery
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The clock window is bilateral, so a delivery dated too far ahead is refused exactly like one dated
|
|
92
|
+
too far behind, because a window that only looked backwards is one a sender widens by dating its own
|
|
93
|
+
delivery in the future. A header the signature covers but the request did not carry is refused
|
|
94
|
+
before any code is computed.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
Every bound one send is held to is yours to set, and every one has a default.
|
|
101
|
+
|
|
102
|
+
| Bound | Default | What it holds back |
|
|
103
|
+
|-|-|-|
|
|
104
|
+
| `max_attempts` | 4 | requests one send issues, capped at 16 whatever a policy says |
|
|
105
|
+
| `initial_backoff` | 100 ms | the ceiling of the wait before the first retry |
|
|
106
|
+
| `max_backoff` | 2 s | the ceiling no single wait between attempts crosses |
|
|
107
|
+
| `max_total_delay` | 5 s | the budget every wait of one send shares |
|
|
108
|
+
| `request_timeout` | 10 s | how long one attempt is given |
|
|
109
|
+
| `max_payload_bytes` | 1 MiB | the payload, refused before a socket is opened |
|
|
110
|
+
| `max_response_bytes` | 8 MiB | the body read off a socket |
|
|
111
|
+
| `max_head_bytes` | 16 KiB | the head of an answer, every line taken together |
|
|
112
|
+
| `max_response_headers` | 64 | header lines one answer may carry |
|
|
113
|
+
| `max_header_bytes` | 64 KiB | one header line |
|
|
114
|
+
|
|
115
|
+
Every default comes from [`clients/conformance/bounds.json`](https://gitlab.com/hook0/hook0/-/blob/master/clients/conformance/bounds.json),
|
|
116
|
+
the corpus every Hook0 SDK reads. A number changed there fails every SDK still carrying the old one,
|
|
117
|
+
so no two of them can bound different things.
|
|
118
|
+
|
|
119
|
+
The last three bound what the other end may cost you. A server that is broken or hostile can
|
|
120
|
+
otherwise stream a head, a header or a body of any length into your process.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
client = Hook0::Client.new(
|
|
124
|
+
"https://app.hook0.com/api/v1",
|
|
125
|
+
application_id,
|
|
126
|
+
token,
|
|
127
|
+
Hook0::Options.new(
|
|
128
|
+
retry_policy: Hook0::RetryPolicy.new(
|
|
129
|
+
max_attempts: 4,
|
|
130
|
+
initial_backoff: 0.1,
|
|
131
|
+
max_backoff: 2.0,
|
|
132
|
+
max_total_delay: 5.0
|
|
133
|
+
),
|
|
134
|
+
request_timeout: 10.0,
|
|
135
|
+
max_payload_bytes: 1024 * 1024,
|
|
136
|
+
max_response_bytes: 8 * 1024 * 1024
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Usage
|
|
144
|
+
|
|
145
|
+
### Sending is idempotent, and retried
|
|
146
|
+
|
|
147
|
+
`send_event` sends every event under an ID it knows, either the one set on the event or a UUIDv7 it
|
|
148
|
+
mints when the event carries none. **Passing no ID does not mean the ID comes from Hook0.** The value comes
|
|
149
|
+
from the client, travels with the request, and is what `send_event` answers.
|
|
150
|
+
|
|
151
|
+
That is what makes a retry safe. Hook0 keys events on their ID, so a request repeated after a network
|
|
152
|
+
failure or a server error ingests the event once rather than twice. Without a client-chosen ID, the
|
|
153
|
+
repeated request would create a second event and deliver it to every subscriber.
|
|
154
|
+
|
|
155
|
+
Retrying is limited to what could end differently. A request that got no answer, a server error and
|
|
156
|
+
an instance saying it is being reached faster than it accepts are all retried. A `429` naming a
|
|
157
|
+
spent quota is not, because a quota clears when a plan changes or a day turns, and no send can wait
|
|
158
|
+
for that. A
|
|
159
|
+
`Retry-After` the answer carries is honoured, clamped to what is left of the delay budget. A retried
|
|
160
|
+
request Hook0 answers with `EventAlreadyIngested` reports success, since an earlier attempt of that
|
|
161
|
+
same send reached the API. The same answer to a *first* attempt is a genuine conflict, and is
|
|
162
|
+
reported as an error.
|
|
163
|
+
|
|
164
|
+
### Declaring the event types you use
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
created = client.upsert_event_types(
|
|
168
|
+
%w[billing.invoice.paid billing.invoice.voided]
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Only the ones your application does not declare yet are created, and those are what comes back.
|
|
173
|
+
|
|
174
|
+
### Calling the rest of the API
|
|
175
|
+
|
|
176
|
+
Every operation the API declares is a method of a generated group, one group per entity.
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
applications = Hook0::Generated::ApplicationsApi.new(
|
|
180
|
+
Hook0::Transport.new("https://app.hook0.com", token)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
begin
|
|
184
|
+
application = applications.get(application_id)
|
|
185
|
+
rescue Hook0::Generated::NotFoundError
|
|
186
|
+
# every problem the API names is its own exception, all of them `ProblemError`
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Names travel as the API spells them
|
|
191
|
+
|
|
192
|
+
A value read out of an answer is one of the classes under `Hook0::Generated`, and `to_h` writes one
|
|
193
|
+
back the way the API reads it. A member Ruby keeps for itself, `method` or `until`, is spelled with
|
|
194
|
+
a trailing underscore in Ruby and under its own name on the wire.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
`clients/ruby/lib/hook0/generated/` is written by [`hook0-sdkgen`](https://gitlab.com/hook0/hook0/-/tree/master/clients/sdkgen)
|
|
201
|
+
from the OpenAPI snapshot the API commits, and is rewritten whole on every regeneration. A hand edit
|
|
202
|
+
there is reverted the next time anyone regenerates, and the drift guard says so before that. Change
|
|
203
|
+
the generator, then run:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
UPDATE_SDK=ruby cargo test -p hook0-sdkgen sdk_targets
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Everything else under `lib/` is hand-written and never regenerated, and so is `test/`.
|
|
210
|
+
|
|
211
|
+
What a send retries, the bounds it is held to and how a signature is verified are dictated by the
|
|
212
|
+
shared corpus at [`clients/conformance`](https://gitlab.com/hook0/hook0/-/tree/master/clients/conformance),
|
|
213
|
+
which every SDK's suite reads, so a verdict changed there fails this client until it agrees again.
|
|
214
|
+
|
|
215
|
+
Every case runs against a real Hook0 over a loopback socket. Nothing here stands in for a part of
|
|
216
|
+
the client.
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
rubocop
|
|
220
|
+
ruby -Ilib -Itest -e 'Dir["test/**/*_test.rb"].sort.each { |f| require File.expand_path(f) }'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
The Hook0 Ruby SDK is free and open source, released under the [MIT License](./LICENSE). Use it,
|
|
228
|
+
change it, ship it, in open source and in commercial work alike, as long as the copyright notice
|
|
229
|
+
travels with it.
|
|
230
|
+
|
|
231
|
+
Hook0 itself is open source too. Read [what Hook0 is](https://documentation.hook0.com/docs/what-is-hook0),
|
|
232
|
+
visit [hook0.com](https://www.hook0.com/), join the [community](https://www.hook0.com/community), or
|
|
233
|
+
write to [support@hook0.com](mailto:support@hook0.com).
|
|
234
|
+
|
|
235
|
+
Maintained by [David Sferruzza](mailto:david@hook0.com) and [François-Guillaume Ribreau](mailto:fg@hook0.com).
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 340" width="900" height="340">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="greenGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#10b981"/><stop offset="100%" stop-color="#059669"/>
|
|
5
|
+
</linearGradient>
|
|
6
|
+
<linearGradient id="indigoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
7
|
+
<stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#4f46e5"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
<linearGradient id="amberGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
10
|
+
<stop offset="0%" stop-color="#f59e0b"/><stop offset="100%" stop-color="#d97706"/>
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<linearGradient id="purpleGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
13
|
+
<stop offset="0%" stop-color="#a855f7"/><stop offset="100%" stop-color="#7c3aed"/>
|
|
14
|
+
</linearGradient>
|
|
15
|
+
|
|
16
|
+
<linearGradient id="flowRight" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
17
|
+
<stop offset="0%" stop-color="#6366f1" stop-opacity="0.15"/>
|
|
18
|
+
<stop offset="45%" stop-color="#6366f1" stop-opacity="0.15"/>
|
|
19
|
+
<stop offset="50%" stop-color="#6366f1" stop-opacity="1"/>
|
|
20
|
+
<stop offset="55%" stop-color="#6366f1" stop-opacity="0.15"/>
|
|
21
|
+
<stop offset="100%" stop-color="#6366f1" stop-opacity="0.15"/>
|
|
22
|
+
<animateTransform attributeName="gradientTransform" type="translate" from="-1 0" to="1 0" dur="2s" repeatCount="indefinite"/>
|
|
23
|
+
</linearGradient>
|
|
24
|
+
<linearGradient id="flowOut" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
25
|
+
<stop offset="0%" stop-color="#a855f7" stop-opacity="0.15"/>
|
|
26
|
+
<stop offset="45%" stop-color="#a855f7" stop-opacity="0.15"/>
|
|
27
|
+
<stop offset="50%" stop-color="#a855f7" stop-opacity="1"/>
|
|
28
|
+
<stop offset="55%" stop-color="#a855f7" stop-opacity="0.15"/>
|
|
29
|
+
<stop offset="100%" stop-color="#a855f7" stop-opacity="0.15"/>
|
|
30
|
+
<animateTransform attributeName="gradientTransform" type="translate" from="-1 0" to="1 0" dur="2s" repeatCount="indefinite" begin="0.6s"/>
|
|
31
|
+
</linearGradient>
|
|
32
|
+
<linearGradient id="flowBack" x1="100%" y1="0%" x2="0%" y2="0%">
|
|
33
|
+
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0.15"/>
|
|
34
|
+
<stop offset="45%" stop-color="#f59e0b" stop-opacity="0.15"/>
|
|
35
|
+
<stop offset="50%" stop-color="#f59e0b" stop-opacity="1"/>
|
|
36
|
+
<stop offset="55%" stop-color="#f59e0b" stop-opacity="0.15"/>
|
|
37
|
+
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0.15"/>
|
|
38
|
+
<animateTransform attributeName="gradientTransform" type="translate" from="1 0" to="-1 0" dur="3s" repeatCount="indefinite" begin="1.2s"/>
|
|
39
|
+
</linearGradient>
|
|
40
|
+
|
|
41
|
+
<filter id="cardShadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
42
|
+
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#0f172a" flood-opacity="0.12"/>
|
|
43
|
+
</filter>
|
|
44
|
+
</defs>
|
|
45
|
+
|
|
46
|
+
<text x="450" y="28" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="600" letter-spacing="1" fill="#64748b">Hook0 Ruby SDK</text>
|
|
47
|
+
<g transform="translate(90, 58)"><rect x="-51.800000000000004" y="-12" width="103.60000000000001" height="20" rx="10" fill="#10b981" fill-opacity="0.1"/><text x="0" y="4" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" font-weight="600" fill="#059669">YOUR CODE</text></g>
|
|
48
|
+
<g transform="translate(20, 75)" filter="url(#cardShadow)">
|
|
49
|
+
<rect width="140" height="100" rx="16" fill="white"/>
|
|
50
|
+
<rect width="140" height="40" rx="16" fill="url(#greenGrad)"/>
|
|
51
|
+
<rect y="28" width="140" height="12" fill="url(#greenGrad)"/>
|
|
52
|
+
<g transform="translate(50, 8)">
|
|
53
|
+
<rect x="0" y="0" width="40" height="24" rx="4" fill="none" stroke="white" stroke-width="2"/>
|
|
54
|
+
<path d="M 12 8 L 8 12 L 12 16" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
55
|
+
<path d="M 28 8 L 32 12 L 28 16" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
56
|
+
</g>
|
|
57
|
+
<text x="70" y="62" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="14" font-weight="600" fill="#1e293b">Your app</text>
|
|
58
|
+
<text x="70" y="80" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="11" fill="#64748b">Something happened</text>
|
|
59
|
+
</g>
|
|
60
|
+
|
|
61
|
+
<g>
|
|
62
|
+
<path d="M 165 125 L 215 125" stroke="#e2e8f0" stroke-width="2" fill="none"/>
|
|
63
|
+
<path d="M 165 125 L 215 125" stroke="url(#flowRight)" stroke-width="3" fill="none"/>
|
|
64
|
+
<path d="M 210 120 L 220 125 L 210 130" fill="none" stroke="#6366f1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
65
|
+
<animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite"/>
|
|
66
|
+
</path>
|
|
67
|
+
</g>
|
|
68
|
+
<g transform="translate(220, 55)" filter="url(#cardShadow)">
|
|
69
|
+
<rect width="190" height="140" rx="16" fill="white"/>
|
|
70
|
+
<rect width="190" height="45" rx="16" fill="url(#indigoGrad)"/>
|
|
71
|
+
<rect y="33" width="190" height="12" fill="url(#indigoGrad)"/>
|
|
72
|
+
<g transform="translate(80, 8)">
|
|
73
|
+
<path d="M 15 3 L 28 10 L 28 22 L 15 29 L 2 22 L 2 10 Z" fill="none" stroke="white" stroke-width="2" stroke-linejoin="round"/>
|
|
74
|
+
<path d="M 2 10 L 15 16 L 28 10 M 15 16 L 15 29" fill="none" stroke="white" stroke-width="2" stroke-linejoin="round"/>
|
|
75
|
+
</g>
|
|
76
|
+
<text x="95" y="68" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="16" font-weight="700" fill="#1e293b">Ruby SDK</text>
|
|
77
|
+
<text x="95" y="86" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">hook0-client</text>
|
|
78
|
+
<g transform="translate(14, 96)"><rect width="76" height="18" rx="9" fill="#6366f1" fill-opacity="0.12"/><text x="38.0" y="13" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="9" fill="#4f46e5">Idempotent</text></g>
|
|
79
|
+
<g transform="translate(100, 96)"><rect width="76" height="18" rx="9" fill="#6366f1" fill-opacity="0.12"/><text x="38.0" y="13" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="9" fill="#4f46e5">Retried</text></g>
|
|
80
|
+
<g transform="translate(52, 118)"><rect width="86" height="18" rx="9" fill="#6366f1" fill-opacity="0.12"/><text x="43.0" y="13" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="9" fill="#4f46e5">Bounded</text></g>
|
|
81
|
+
</g>
|
|
82
|
+
|
|
83
|
+
<g>
|
|
84
|
+
<path d="M 415 125 L 465 125" stroke="#e2e8f0" stroke-width="2" fill="none"/>
|
|
85
|
+
<path d="M 415 125 L 465 125" stroke="url(#flowRight)" stroke-width="3" fill="none"/>
|
|
86
|
+
<path d="M 460 120 L 470 125 L 460 130" fill="none" stroke="#6366f1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
87
|
+
<animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite" begin="0.3s"/>
|
|
88
|
+
</path>
|
|
89
|
+
</g>
|
|
90
|
+
<g transform="translate(470, 70)" filter="url(#cardShadow)">
|
|
91
|
+
<rect width="160" height="110" rx="16" fill="white"/>
|
|
92
|
+
<rect width="160" height="40" rx="16" fill="url(#indigoGrad)"/>
|
|
93
|
+
<rect y="28" width="160" height="12" fill="url(#indigoGrad)"/>
|
|
94
|
+
<g transform="translate(66, 6) scale(0.06981) translate(-64.194, -69.194)">
|
|
95
|
+
<path d="M308.629 398.482L287.416 419.696L110.64 242.919L131.853 221.706L308.629 398.482ZM393.482 398.482C416.914 375.051 416.914 337.061 393.482 313.63L216.706 136.853C193.274 113.421 155.284 113.421 131.853 136.853C108.421 160.284 108.421 198.274 131.853 221.706L110.64 242.919L109.823 242.091C75.4948 206.882 75.7669 150.512 110.64 115.64C145.512 80.767 201.882 80.4949 237.091 114.823L237.919 115.64L414.695 292.416C449.843 327.563 449.843 384.548 414.695 419.696C379.548 454.843 322.563 454.843 287.416 419.696L308.629 398.482C332.061 421.914 370.051 421.914 393.482 398.482Z" fill="white"/>
|
|
96
|
+
<path d="M266.544 186.938L181.691 271.791L181.571 271.671L160.358 292.885L160.477 293.004L135.852 317.63C112.421 341.061 112.421 379.051 135.852 402.483C159.284 425.914 197.274 425.914 220.706 402.483L245.33 377.858L266.543 399.07L241.919 423.695L241.091 424.513C205.882 458.84 149.512 458.568 114.639 423.695C80.1531 389.209 79.506 333.699 112.696 298.422L112.546 298.272L229.817 181L229.935 181.118L245.331 165.725L266.544 186.938ZM291.416 119.64C326.563 84.4927 383.548 84.4929 418.695 119.64C453.842 154.787 453.842 211.772 418.695 246.919L412.606 253.007L412.812 253.213L295.541 370.484L295.335 370.278L287.757 377.857L266.544 356.644L351.396 271.791L351.602 271.997L372.815 250.784L372.609 250.578L397.482 225.706C420.914 202.275 420.914 164.284 397.482 140.853C374.051 117.422 336.061 117.422 312.63 140.853L287.757 165.725L266.544 144.512L291.416 119.64Z" fill="white"/>
|
|
97
|
+
<path d="M181.5 271.5L160 293L168 300.5L190 280L181.5 271.5Z" fill="white"/>
|
|
98
|
+
<path d="M352.5 272.5L373.5 251.5L366 244L344 264.5L352.5 272.5Z" fill="white"/>
|
|
99
|
+
<path d="M267 356.5L288 377.5L295.5 370L275 348L267 356.5Z" fill="white"/>
|
|
100
|
+
<path d="M139.5 272L161 250.5L153 243L131 263.5L139.5 272Z" fill="white"/>
|
|
101
|
+
</g>
|
|
102
|
+
<text x="80" y="62" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="16" font-weight="700" fill="#1e293b">Hook0</text>
|
|
103
|
+
<text x="80" y="79" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">Webhooks-as-a-Service</text>
|
|
104
|
+
<g transform="translate(20, 88)"><rect width="120" height="18" rx="9" fill="#6366f1" fill-opacity="0.12"/><text x="60.0" y="13" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="9" fill="#4f46e5">Fan-out, retries, history</text></g>
|
|
105
|
+
</g>
|
|
106
|
+
|
|
107
|
+
<g>
|
|
108
|
+
<path d="M 635 125 L 685 125" stroke="#e2e8f0" stroke-width="2" fill="none"/>
|
|
109
|
+
<path d="M 635 125 L 685 125" stroke="url(#flowOut)" stroke-width="3" fill="none"/>
|
|
110
|
+
<path d="M 680 120 L 690 125 L 680 130" fill="none" stroke="#a855f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
111
|
+
<animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite"/>
|
|
112
|
+
</path>
|
|
113
|
+
</g>
|
|
114
|
+
<g transform="translate(785, 58)"><rect x="-57.0" y="-12" width="114.0" height="20" rx="10" fill="#a855f7" fill-opacity="0.1"/><text x="0" y="4" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" font-weight="600" fill="#7c3aed">YOUR USERS</text></g>
|
|
115
|
+
<g transform="translate(690, 75)" filter="url(#cardShadow)">
|
|
116
|
+
<rect width="190" height="100" rx="16" fill="white"/>
|
|
117
|
+
<rect width="190" height="40" rx="16" fill="url(#purpleGrad)"/>
|
|
118
|
+
<rect y="28" width="190" height="12" fill="url(#purpleGrad)"/>
|
|
119
|
+
<g transform="translate(75, 8)">
|
|
120
|
+
<path d="M 4 6 L 36 6 L 36 22 L 4 22 Z" fill="none" stroke="white" stroke-width="2" stroke-linejoin="round"/>
|
|
121
|
+
<path d="M 4 6 L 20 17 L 36 6" fill="none" stroke="white" stroke-width="2" stroke-linejoin="round"/>
|
|
122
|
+
</g>
|
|
123
|
+
<text x="95" y="62" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="14" font-weight="600" fill="#1e293b">Their endpoints</text>
|
|
124
|
+
<g transform="translate(35, 72)"><rect width="120" height="18" rx="9" fill="#a855f7" fill-opacity="0.12"/><text x="60.0" y="13" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="9" fill="#7c3aed">Signature verified</text></g>
|
|
125
|
+
</g>
|
|
126
|
+
|
|
127
|
+
<g>
|
|
128
|
+
<path d="M 785 180 L 785 210 L 260 210 L 260 201" stroke="#e2e8f0" stroke-width="2" fill="none"/>
|
|
129
|
+
<path d="M 785 180 L 785 210 L 260 210 L 260 201" stroke="url(#flowBack)" stroke-width="3" fill="none"/>
|
|
130
|
+
<path d="M 255 206 L 260 197 L 265 206" fill="none" stroke="#d97706" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
131
|
+
<animate attributeName="opacity" values="0.3;1;0.3" dur="3s" repeatCount="indefinite" begin="1.2s"/>
|
|
132
|
+
</path>
|
|
133
|
+
<rect x="418" y="198" width="244" height="14" fill="white"/>
|
|
134
|
+
<text x="540" y="209" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#94a3b8">the same package verifies what comes back</text>
|
|
135
|
+
</g>
|
|
136
|
+
<rect x="20" y="226" width="860" height="98" rx="14" fill="#f8fafc"/>
|
|
137
|
+
<text x="450" y="247" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="9" font-weight="600" letter-spacing="1.5" fill="#94a3b8">IN THE PACKAGE</text>
|
|
138
|
+
<text x="106.0" y="272" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="700" fill="#334155">Send</text>
|
|
139
|
+
<text x="106.0" y="290" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">Events, under an ID</text>
|
|
140
|
+
<text x="106.0" y="304" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">the client minted</text>
|
|
141
|
+
<text x="278.0" y="272" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="700" fill="#334155">Retry</text>
|
|
142
|
+
<text x="278.0" y="290" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">Only what could end</text>
|
|
143
|
+
<text x="278.0" y="304" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">differently, bounded</text>
|
|
144
|
+
<text x="450.0" y="272" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="700" fill="#334155">Declare</text>
|
|
145
|
+
<text x="450.0" y="290" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">Upsert the event types</text>
|
|
146
|
+
<text x="450.0" y="304" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">your application uses</text>
|
|
147
|
+
<text x="622.0" y="272" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="700" fill="#334155">Verify</text>
|
|
148
|
+
<text x="622.0" y="290" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">HMAC-SHA256, on a</text>
|
|
149
|
+
<text x="622.0" y="304" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">bilateral clock window</text>
|
|
150
|
+
<text x="794.0" y="272" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="700" fill="#334155">Call</text>
|
|
151
|
+
<text x="794.0" y="290" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">Every operation the</text>
|
|
152
|
+
<text x="794.0" y="304" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="10" fill="#64748b">API declares, typed</text>
|
|
153
|
+
</svg>
|