jobcelis 1.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/LICENSE +21 -0
- data/README.md +376 -0
- data/lib/jobcelis/client.rb +662 -0
- data/lib/jobcelis/error.rb +13 -0
- data/lib/jobcelis/webhook_verifier.rb +20 -0
- data/lib/jobcelis.rb +9 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f5e335df49a6f6251dc82ca2884ce9cfa1541ef94fe9936c19344069c1bf68f5
|
|
4
|
+
data.tar.gz: a1dcc32aa94793cc608150b7533fec44fd4117f11224c8e389459529da2153c3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 20cb85eba443c66a9e28f6a13587803096a852c00faa66167e51810d8c55b375f29b5c4a1d6a591c29a3e40ed02ad33d160db9b40608f01a22fe20dd510f827b
|
|
7
|
+
data.tar.gz: 0e1fdb03d0370f9e7903d58475ef8cd77fc995ae3e52f54da3adbeff0b6f5f65211bce5745cfab4e5b2ff7f2a31b44bd6aceced228799a8e5837e9f10c469171
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jobcelis
|
|
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,376 @@
|
|
|
1
|
+
# jobcelis
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for the [Jobcelis](https://jobcelis.com) Event Infrastructure Platform.
|
|
4
|
+
|
|
5
|
+
All API calls go to `https://jobcelis.com` by default -- you only need your API key to get started.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "jobcelis"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install jobcelis
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "jobcelis"
|
|
31
|
+
|
|
32
|
+
# Only your API key is required -- connects to https://jobcelis.com automatically
|
|
33
|
+
client = Jobcelis::Client.new(api_key: "your_api_key")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> **Custom URL:** If you're self-hosting Jobcelis, you can override the base URL:
|
|
37
|
+
> ```ruby
|
|
38
|
+
> client = Jobcelis::Client.new(api_key: "your_api_key", base_url: "https://your-instance.example.com")
|
|
39
|
+
> ```
|
|
40
|
+
|
|
41
|
+
## Authentication
|
|
42
|
+
|
|
43
|
+
The auth methods do not require an API key. Use them to register, log in, and manage JWT tokens.
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
require "jobcelis"
|
|
47
|
+
|
|
48
|
+
client = Jobcelis::Client.new(api_key: "")
|
|
49
|
+
|
|
50
|
+
# Register a new account
|
|
51
|
+
user = client.register(email: "alice@example.com", password: "SecurePass123!", name: "Alice")
|
|
52
|
+
|
|
53
|
+
# Log in -- returns JWT access token and refresh token
|
|
54
|
+
session = client.login(email: "alice@example.com", password: "SecurePass123!")
|
|
55
|
+
access_token = session["token"]
|
|
56
|
+
refresh_tok = session["refresh_token"]
|
|
57
|
+
|
|
58
|
+
# Set the JWT for subsequent authenticated calls
|
|
59
|
+
client.set_auth_token(access_token)
|
|
60
|
+
|
|
61
|
+
# Refresh an expired token
|
|
62
|
+
new_session = client.refresh_token(refresh_tok)
|
|
63
|
+
client.set_auth_token(new_session["token"])
|
|
64
|
+
|
|
65
|
+
# Verify MFA (requires Bearer token already set)
|
|
66
|
+
result = client.verify_mfa(token: access_token, code: "123456")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Events
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# Send a single event
|
|
73
|
+
event = client.send_event("order.created", { order_id: "123", amount: 99.99 })
|
|
74
|
+
|
|
75
|
+
# Send batch events (up to 1000)
|
|
76
|
+
batch = client.send_events([
|
|
77
|
+
{ topic: "order.created", payload: { order_id: "1" } },
|
|
78
|
+
{ topic: "order.created", payload: { order_id: "2" } },
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
# List events with pagination
|
|
82
|
+
events = client.list_events(limit: 25)
|
|
83
|
+
next_page = client.list_events(limit: 25, cursor: events["cursor"])
|
|
84
|
+
|
|
85
|
+
# Get / delete a single event
|
|
86
|
+
event = client.get_event("evt_abc123")
|
|
87
|
+
client.delete_event("evt_abc123")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Simulate
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# Dry-run an event to see which webhooks would fire
|
|
94
|
+
result = client.simulate_event("order.created", { order_id: "test" })
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Webhooks
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# Create a webhook
|
|
101
|
+
webhook = client.create_webhook(
|
|
102
|
+
url: "https://example.com/webhook",
|
|
103
|
+
topics: ["order.*"],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# List, get, update, delete
|
|
107
|
+
webhooks = client.list_webhooks
|
|
108
|
+
wh = client.get_webhook("wh_abc123")
|
|
109
|
+
client.update_webhook("wh_abc123", url: "https://new-url.com/hook")
|
|
110
|
+
client.delete_webhook("wh_abc123")
|
|
111
|
+
|
|
112
|
+
# Health and templates
|
|
113
|
+
health = client.webhook_health("wh_abc123")
|
|
114
|
+
templates = client.webhook_templates
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Deliveries
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
deliveries = client.list_deliveries(limit: 20, status: "failed")
|
|
121
|
+
client.retry_delivery("del_abc123")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Dead Letters
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
dead_letters = client.list_dead_letters
|
|
128
|
+
dl = client.get_dead_letter("dlq_abc123")
|
|
129
|
+
client.retry_dead_letter("dlq_abc123")
|
|
130
|
+
client.resolve_dead_letter("dlq_abc123")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Replays
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
replay = client.create_replay(
|
|
137
|
+
topic: "order.created",
|
|
138
|
+
from_date: "2026-01-01T00:00:00Z",
|
|
139
|
+
to_date: "2026-01-31T23:59:59Z",
|
|
140
|
+
webhook_id: "wh_abc123", # optional
|
|
141
|
+
)
|
|
142
|
+
replays = client.list_replays
|
|
143
|
+
r = client.get_replay("rpl_abc123")
|
|
144
|
+
client.cancel_replay("rpl_abc123")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Scheduled Jobs
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# Create a job
|
|
151
|
+
job = client.create_job(
|
|
152
|
+
name: "daily-report",
|
|
153
|
+
queue: "default",
|
|
154
|
+
cron_expression: "0 9 * * *",
|
|
155
|
+
payload: { type: "daily" },
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# CRUD
|
|
159
|
+
jobs = client.list_jobs(limit: 10)
|
|
160
|
+
job = client.get_job("job_abc123")
|
|
161
|
+
client.update_job("job_abc123", cron_expression: "0 10 * * *")
|
|
162
|
+
client.delete_job("job_abc123")
|
|
163
|
+
|
|
164
|
+
# List runs for a job
|
|
165
|
+
runs = client.list_job_runs("job_abc123", limit: 20)
|
|
166
|
+
|
|
167
|
+
# Preview cron schedule
|
|
168
|
+
preview = client.cron_preview("0 9 * * *", count: 10)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Pipelines
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
pipeline = client.create_pipeline(
|
|
175
|
+
name: "order-processing",
|
|
176
|
+
topics: ["order.created"],
|
|
177
|
+
steps: [
|
|
178
|
+
{ type: "filter", config: { field: "amount", gt: 100 } },
|
|
179
|
+
{ type: "transform", config: { add_field: "priority", value: "high" } },
|
|
180
|
+
],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
pipelines = client.list_pipelines
|
|
184
|
+
p = client.get_pipeline("pipe_abc123")
|
|
185
|
+
client.update_pipeline("pipe_abc123", name: "order-processing-v2")
|
|
186
|
+
client.delete_pipeline("pipe_abc123")
|
|
187
|
+
|
|
188
|
+
# Test a pipeline with a sample payload
|
|
189
|
+
result = client.test_pipeline("pipe_abc123", { topic: "order.created", payload: { id: "1" } })
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Event Schemas
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
schema = client.create_event_schema(
|
|
196
|
+
topic: "order.created",
|
|
197
|
+
schema: {
|
|
198
|
+
type: "object",
|
|
199
|
+
properties: {
|
|
200
|
+
order_id: { type: "string" },
|
|
201
|
+
amount: { type: "number" },
|
|
202
|
+
},
|
|
203
|
+
required: ["order_id", "amount"],
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
schemas = client.list_event_schemas
|
|
208
|
+
s = client.get_event_schema("sch_abc123")
|
|
209
|
+
client.update_event_schema("sch_abc123", schema: { type: "object" })
|
|
210
|
+
client.delete_event_schema("sch_abc123")
|
|
211
|
+
|
|
212
|
+
# Validate a payload against a topic's schema
|
|
213
|
+
result = client.validate_payload("order.created", { order_id: "123", amount: 50 })
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Sandbox
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# Create a temporary endpoint for testing
|
|
220
|
+
endpoint = client.create_sandbox_endpoint(name: "my-test")
|
|
221
|
+
endpoints = client.list_sandbox_endpoints
|
|
222
|
+
|
|
223
|
+
# Inspect received requests
|
|
224
|
+
requests = client.list_sandbox_requests("sbx_abc123", limit: 20)
|
|
225
|
+
|
|
226
|
+
client.delete_sandbox_endpoint("sbx_abc123")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Analytics
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
events_chart = client.events_per_day(days: 30)
|
|
233
|
+
deliveries_chart = client.deliveries_per_day(days: 7)
|
|
234
|
+
topics = client.top_topics(limit: 5)
|
|
235
|
+
stats = client.webhook_stats
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Project and Token Management
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# Current project
|
|
242
|
+
project = client.get_project
|
|
243
|
+
client.update_project(name: "My Project v2")
|
|
244
|
+
|
|
245
|
+
# Topics
|
|
246
|
+
topics = client.list_topics
|
|
247
|
+
|
|
248
|
+
# API token
|
|
249
|
+
token = client.get_token
|
|
250
|
+
new_token = client.regenerate_token
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Multi-Project Management
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
projects = client.list_projects
|
|
257
|
+
new_project = client.create_project("staging-env")
|
|
258
|
+
p = client.get_project_by_id("proj_abc123")
|
|
259
|
+
client.update_project_by_id("proj_abc123", name: "production-env")
|
|
260
|
+
client.set_default_project("proj_abc123")
|
|
261
|
+
client.delete_project("proj_abc123")
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Team Members
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
members = client.list_members("proj_abc123")
|
|
268
|
+
member = client.add_member("proj_abc123", email: "alice@example.com", role: "admin")
|
|
269
|
+
client.update_member("proj_abc123", "mem_abc123", role: "viewer")
|
|
270
|
+
client.remove_member("proj_abc123", "mem_abc123")
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Invitations
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
# List pending invitations
|
|
277
|
+
invitations = client.list_pending_invitations
|
|
278
|
+
|
|
279
|
+
# Accept or reject
|
|
280
|
+
client.accept_invitation("inv_abc123")
|
|
281
|
+
client.reject_invitation("inv_def456")
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Audit Logs
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
logs = client.list_audit_logs(limit: 100)
|
|
288
|
+
next_page = client.list_audit_logs(cursor: logs["cursor"])
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Data Export
|
|
292
|
+
|
|
293
|
+
Export methods return raw strings (CSV or JSON).
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
# Export as CSV
|
|
297
|
+
csv_data = client.export_events(format: "csv")
|
|
298
|
+
File.write("events.csv", csv_data)
|
|
299
|
+
|
|
300
|
+
# Export as JSON
|
|
301
|
+
json_data = client.export_deliveries(format: "json")
|
|
302
|
+
|
|
303
|
+
# Other exports
|
|
304
|
+
client.export_jobs(format: "csv")
|
|
305
|
+
client.export_audit_log(format: "csv")
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## GDPR / Privacy
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
# Consent management
|
|
312
|
+
consents = client.get_consents
|
|
313
|
+
client.accept_consent("marketing")
|
|
314
|
+
|
|
315
|
+
# Data portability
|
|
316
|
+
my_data = client.export_my_data
|
|
317
|
+
|
|
318
|
+
# Processing restrictions
|
|
319
|
+
client.restrict_processing
|
|
320
|
+
client.lift_restriction
|
|
321
|
+
|
|
322
|
+
# Right to object
|
|
323
|
+
client.object_to_processing
|
|
324
|
+
client.restore_consent
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Health Check
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
health = client.health
|
|
331
|
+
status = client.status
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Error Handling
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
require "jobcelis"
|
|
338
|
+
|
|
339
|
+
client = Jobcelis::Client.new(api_key: "your_api_key")
|
|
340
|
+
|
|
341
|
+
begin
|
|
342
|
+
event = client.get_event("nonexistent")
|
|
343
|
+
rescue Jobcelis::Error => e
|
|
344
|
+
puts "Status: #{e.status}" # 404
|
|
345
|
+
puts "Detail: #{e.detail}" # {"message"=>"Not found"}
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Webhook Signature Verification
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
require "jobcelis"
|
|
353
|
+
|
|
354
|
+
# Sinatra example
|
|
355
|
+
post "/webhook" do
|
|
356
|
+
body = request.body.read
|
|
357
|
+
signature = request.env["HTTP_X_SIGNATURE"]
|
|
358
|
+
|
|
359
|
+
unless Jobcelis::WebhookVerifier.verify(
|
|
360
|
+
secret: "your_webhook_secret",
|
|
361
|
+
body: body,
|
|
362
|
+
signature: signature
|
|
363
|
+
)
|
|
364
|
+
halt 401, "Invalid signature"
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
event = JSON.parse(body)
|
|
368
|
+
puts "Received: #{event['topic']}"
|
|
369
|
+
status 200
|
|
370
|
+
"OK"
|
|
371
|
+
end
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## License
|
|
375
|
+
|
|
376
|
+
MIT
|
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Jobcelis
|
|
8
|
+
# Client for the Jobcelis Event Infrastructure Platform API.
|
|
9
|
+
#
|
|
10
|
+
# All API calls go to https://jobcelis.com by default.
|
|
11
|
+
#
|
|
12
|
+
# client = Jobcelis::Client.new(api_key: "your_api_key")
|
|
13
|
+
#
|
|
14
|
+
class Client
|
|
15
|
+
# @param api_key [String] Your project API key.
|
|
16
|
+
# @param base_url [String] Base URL of the Jobcelis API (default: https://jobcelis.com).
|
|
17
|
+
# @param timeout [Integer] Request timeout in seconds (default: 30).
|
|
18
|
+
def initialize(api_key:, base_url: "https://jobcelis.com", timeout: 30)
|
|
19
|
+
@api_key = api_key
|
|
20
|
+
@base_url = base_url.chomp("/")
|
|
21
|
+
@timeout = timeout
|
|
22
|
+
@auth_token = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
# Auth
|
|
27
|
+
# ------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
# Register a new account. Does not use API key auth.
|
|
30
|
+
def register(email:, password:, name: nil)
|
|
31
|
+
body = { email: email, password: password }
|
|
32
|
+
body[:name] = name unless name.nil?
|
|
33
|
+
public_post("/api/v1/auth/register", body)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Log in and receive JWT + refresh token. Does not use API key auth.
|
|
37
|
+
def login(email:, password:)
|
|
38
|
+
public_post("/api/v1/auth/login", { email: email, password: password })
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Refresh an expired JWT using a refresh token. Does not use API key auth.
|
|
42
|
+
def refresh_token(refresh_token)
|
|
43
|
+
public_post("/api/v1/auth/refresh", { refresh_token: refresh_token })
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Verify MFA code. Requires Bearer token set via set_auth_token.
|
|
47
|
+
def verify_mfa(token:, code:)
|
|
48
|
+
post("/api/v1/auth/mfa/verify", { token: token, code: code })
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Set JWT bearer token for authenticated requests.
|
|
52
|
+
def set_auth_token(token)
|
|
53
|
+
@auth_token = token
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
# Events
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
# Send a single event.
|
|
61
|
+
def send_event(topic, payload, **kwargs)
|
|
62
|
+
post("/api/v1/events", { topic: topic, payload: payload, **kwargs })
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Send up to 1000 events in a batch.
|
|
66
|
+
def send_events(events)
|
|
67
|
+
post("/api/v1/events/batch", { events: events })
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Get event details.
|
|
71
|
+
def get_event(event_id)
|
|
72
|
+
get("/api/v1/events/#{event_id}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# List events with cursor pagination.
|
|
76
|
+
def list_events(limit: 50, cursor: nil)
|
|
77
|
+
get("/api/v1/events", { limit: limit, cursor: cursor })
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Deactivate an event.
|
|
81
|
+
def delete_event(event_id)
|
|
82
|
+
do_delete("/api/v1/events/#{event_id}")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
# Simulate
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
# Simulate sending an event (dry run).
|
|
90
|
+
def simulate_event(topic, payload)
|
|
91
|
+
post("/api/v1/simulate", { topic: topic, payload: payload })
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
# Webhooks
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
# Create a webhook.
|
|
99
|
+
def create_webhook(url:, **kwargs)
|
|
100
|
+
post("/api/v1/webhooks", { url: url, **kwargs })
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get webhook details.
|
|
104
|
+
def get_webhook(webhook_id)
|
|
105
|
+
get("/api/v1/webhooks/#{webhook_id}")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# List webhooks.
|
|
109
|
+
def list_webhooks(limit: 50, cursor: nil)
|
|
110
|
+
get("/api/v1/webhooks", { limit: limit, cursor: cursor })
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Update a webhook.
|
|
114
|
+
def update_webhook(webhook_id, **kwargs)
|
|
115
|
+
patch("/api/v1/webhooks/#{webhook_id}", kwargs)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Deactivate a webhook.
|
|
119
|
+
def delete_webhook(webhook_id)
|
|
120
|
+
do_delete("/api/v1/webhooks/#{webhook_id}")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get health status for a webhook.
|
|
124
|
+
def webhook_health(webhook_id)
|
|
125
|
+
get("/api/v1/webhooks/#{webhook_id}/health")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# List available webhook templates.
|
|
129
|
+
def webhook_templates
|
|
130
|
+
get("/api/v1/webhooks/templates")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# Deliveries
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
# List deliveries.
|
|
138
|
+
def list_deliveries(limit: 50, cursor: nil, **filters)
|
|
139
|
+
get("/api/v1/deliveries", { limit: limit, cursor: cursor, **filters })
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Retry a failed delivery.
|
|
143
|
+
def retry_delivery(delivery_id)
|
|
144
|
+
post("/api/v1/deliveries/#{delivery_id}/retry", {})
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
# Dead Letters
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
# List dead letters.
|
|
152
|
+
def list_dead_letters(limit: 50, cursor: nil)
|
|
153
|
+
get("/api/v1/dead-letters", { limit: limit, cursor: cursor })
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Get dead letter details.
|
|
157
|
+
def get_dead_letter(dead_letter_id)
|
|
158
|
+
get("/api/v1/dead-letters/#{dead_letter_id}")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Retry a dead letter.
|
|
162
|
+
def retry_dead_letter(dead_letter_id)
|
|
163
|
+
post("/api/v1/dead-letters/#{dead_letter_id}/retry", {})
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Mark a dead letter as resolved.
|
|
167
|
+
def resolve_dead_letter(dead_letter_id)
|
|
168
|
+
patch("/api/v1/dead-letters/#{dead_letter_id}/resolve", {})
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
# Replays
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
# Start an event replay.
|
|
176
|
+
def create_replay(topic:, from_date:, to_date:, webhook_id: nil)
|
|
177
|
+
body = { topic: topic, from_date: from_date, to_date: to_date }
|
|
178
|
+
body[:webhook_id] = webhook_id unless webhook_id.nil?
|
|
179
|
+
post("/api/v1/replays", body)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# List replays.
|
|
183
|
+
def list_replays(limit: 50, cursor: nil)
|
|
184
|
+
get("/api/v1/replays", { limit: limit, cursor: cursor })
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Get replay details.
|
|
188
|
+
def get_replay(replay_id)
|
|
189
|
+
get("/api/v1/replays/#{replay_id}")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Cancel a replay.
|
|
193
|
+
def cancel_replay(replay_id)
|
|
194
|
+
do_delete("/api/v1/replays/#{replay_id}")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
# Jobs
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
# Create a scheduled job.
|
|
202
|
+
def create_job(name:, queue:, cron_expression:, **kwargs)
|
|
203
|
+
post("/api/v1/jobs", { name: name, queue: queue, cron_expression: cron_expression, **kwargs })
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# List scheduled jobs.
|
|
207
|
+
def list_jobs(limit: 50, cursor: nil)
|
|
208
|
+
get("/api/v1/jobs", { limit: limit, cursor: cursor })
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Get job details.
|
|
212
|
+
def get_job(job_id)
|
|
213
|
+
get("/api/v1/jobs/#{job_id}")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Update a scheduled job.
|
|
217
|
+
def update_job(job_id, **kwargs)
|
|
218
|
+
patch("/api/v1/jobs/#{job_id}", kwargs)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Delete a scheduled job.
|
|
222
|
+
def delete_job(job_id)
|
|
223
|
+
do_delete("/api/v1/jobs/#{job_id}")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# List runs for a scheduled job.
|
|
227
|
+
def list_job_runs(job_id, limit: 50)
|
|
228
|
+
get("/api/v1/jobs/#{job_id}/runs", { limit: limit })
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Preview next occurrences for a cron expression.
|
|
232
|
+
def cron_preview(expression, count: 5)
|
|
233
|
+
get("/api/v1/jobs/cron-preview", { expression: expression, count: count })
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# ------------------------------------------------------------------
|
|
237
|
+
# Pipelines
|
|
238
|
+
# ------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
# Create an event pipeline.
|
|
241
|
+
def create_pipeline(name:, topics:, steps:, **kwargs)
|
|
242
|
+
post("/api/v1/pipelines", { name: name, topics: topics, steps: steps, **kwargs })
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# List pipelines.
|
|
246
|
+
def list_pipelines(limit: 50, cursor: nil)
|
|
247
|
+
get("/api/v1/pipelines", { limit: limit, cursor: cursor })
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Get pipeline details.
|
|
251
|
+
def get_pipeline(pipeline_id)
|
|
252
|
+
get("/api/v1/pipelines/#{pipeline_id}")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Update a pipeline.
|
|
256
|
+
def update_pipeline(pipeline_id, **kwargs)
|
|
257
|
+
patch("/api/v1/pipelines/#{pipeline_id}", kwargs)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Delete a pipeline.
|
|
261
|
+
def delete_pipeline(pipeline_id)
|
|
262
|
+
do_delete("/api/v1/pipelines/#{pipeline_id}")
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Test a pipeline with a sample payload.
|
|
266
|
+
def test_pipeline(pipeline_id, payload)
|
|
267
|
+
post("/api/v1/pipelines/#{pipeline_id}/test", payload)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# ------------------------------------------------------------------
|
|
271
|
+
# Event Schemas
|
|
272
|
+
# ------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
# Create an event schema.
|
|
275
|
+
def create_event_schema(topic:, schema:, **kwargs)
|
|
276
|
+
post("/api/v1/event-schemas", { topic: topic, schema: schema, **kwargs })
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# List event schemas.
|
|
280
|
+
def list_event_schemas(limit: 50, cursor: nil)
|
|
281
|
+
get("/api/v1/event-schemas", { limit: limit, cursor: cursor })
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Get event schema details.
|
|
285
|
+
def get_event_schema(schema_id)
|
|
286
|
+
get("/api/v1/event-schemas/#{schema_id}")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Update an event schema.
|
|
290
|
+
def update_event_schema(schema_id, **kwargs)
|
|
291
|
+
patch("/api/v1/event-schemas/#{schema_id}", kwargs)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Delete an event schema.
|
|
295
|
+
def delete_event_schema(schema_id)
|
|
296
|
+
do_delete("/api/v1/event-schemas/#{schema_id}")
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Validate a payload against the schema for a topic.
|
|
300
|
+
def validate_payload(topic, payload)
|
|
301
|
+
post("/api/v1/event-schemas/validate", { topic: topic, payload: payload })
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# ------------------------------------------------------------------
|
|
305
|
+
# Sandbox
|
|
306
|
+
# ------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
# List sandbox endpoints.
|
|
309
|
+
def list_sandbox_endpoints
|
|
310
|
+
get("/api/v1/sandbox-endpoints")
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Create a sandbox endpoint.
|
|
314
|
+
def create_sandbox_endpoint(name: nil)
|
|
315
|
+
body = {}
|
|
316
|
+
body[:name] = name unless name.nil?
|
|
317
|
+
post("/api/v1/sandbox-endpoints", body)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Delete a sandbox endpoint.
|
|
321
|
+
def delete_sandbox_endpoint(endpoint_id)
|
|
322
|
+
do_delete("/api/v1/sandbox-endpoints/#{endpoint_id}")
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# List requests received by a sandbox endpoint.
|
|
326
|
+
def list_sandbox_requests(endpoint_id, limit: 50)
|
|
327
|
+
get("/api/v1/sandbox-endpoints/#{endpoint_id}/requests", { limit: limit })
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# ------------------------------------------------------------------
|
|
331
|
+
# Analytics
|
|
332
|
+
# ------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
# Get events per day for the last N days.
|
|
335
|
+
def events_per_day(days: 7)
|
|
336
|
+
get("/api/v1/analytics/events-per-day", { days: days })
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Get deliveries per day for the last N days.
|
|
340
|
+
def deliveries_per_day(days: 7)
|
|
341
|
+
get("/api/v1/analytics/deliveries-per-day", { days: days })
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Get top topics by event count.
|
|
345
|
+
def top_topics(limit: 10)
|
|
346
|
+
get("/api/v1/analytics/top-topics", { limit: limit })
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Get webhook delivery statistics.
|
|
350
|
+
def webhook_stats
|
|
351
|
+
get("/api/v1/analytics/webhook-stats")
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# ------------------------------------------------------------------
|
|
355
|
+
# Project (single / current)
|
|
356
|
+
# ------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
# Get current project details.
|
|
359
|
+
def get_project
|
|
360
|
+
get("/api/v1/project")
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Update current project.
|
|
364
|
+
def update_project(**kwargs)
|
|
365
|
+
patch("/api/v1/project", kwargs)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# List all topics in the current project.
|
|
369
|
+
def list_topics
|
|
370
|
+
get("/api/v1/topics")
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Get the current API token info.
|
|
374
|
+
def get_token
|
|
375
|
+
get("/api/v1/token")
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Regenerate the API token.
|
|
379
|
+
def regenerate_token
|
|
380
|
+
post("/api/v1/token/regenerate", {})
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# ------------------------------------------------------------------
|
|
384
|
+
# Projects (multi)
|
|
385
|
+
# ------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
# List all projects.
|
|
388
|
+
def list_projects
|
|
389
|
+
get("/api/v1/projects")
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Create a new project.
|
|
393
|
+
def create_project(name)
|
|
394
|
+
post("/api/v1/projects", { name: name })
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Get project by ID.
|
|
398
|
+
def get_project_by_id(project_id)
|
|
399
|
+
get("/api/v1/projects/#{project_id}")
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Update a project by ID.
|
|
403
|
+
def update_project_by_id(project_id, **kwargs)
|
|
404
|
+
patch("/api/v1/projects/#{project_id}", kwargs)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Delete a project.
|
|
408
|
+
def delete_project(project_id)
|
|
409
|
+
do_delete("/api/v1/projects/#{project_id}")
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Set a project as the default.
|
|
413
|
+
def set_default_project(project_id)
|
|
414
|
+
patch("/api/v1/projects/#{project_id}/default", {})
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# ------------------------------------------------------------------
|
|
418
|
+
# Teams
|
|
419
|
+
# ------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
# List members of a project.
|
|
422
|
+
def list_members(project_id)
|
|
423
|
+
get("/api/v1/projects/#{project_id}/members")
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Add a member to a project.
|
|
427
|
+
def add_member(project_id, email:, role: "member")
|
|
428
|
+
post("/api/v1/projects/#{project_id}/members", { email: email, role: role })
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Update a member's role.
|
|
432
|
+
def update_member(project_id, member_id, role:)
|
|
433
|
+
patch("/api/v1/projects/#{project_id}/members/#{member_id}", { role: role })
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Remove a member from a project.
|
|
437
|
+
def remove_member(project_id, member_id)
|
|
438
|
+
do_delete("/api/v1/projects/#{project_id}/members/#{member_id}")
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# ------------------------------------------------------------------
|
|
442
|
+
# Invitations
|
|
443
|
+
# ------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
# List pending invitations for the current user.
|
|
446
|
+
def list_pending_invitations
|
|
447
|
+
get("/api/v1/invitations/pending")
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Accept an invitation.
|
|
451
|
+
def accept_invitation(invitation_id)
|
|
452
|
+
post("/api/v1/invitations/#{invitation_id}/accept", {})
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Reject an invitation.
|
|
456
|
+
def reject_invitation(invitation_id)
|
|
457
|
+
post("/api/v1/invitations/#{invitation_id}/reject", {})
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# ------------------------------------------------------------------
|
|
461
|
+
# Audit
|
|
462
|
+
# ------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
# List audit log entries.
|
|
465
|
+
def list_audit_logs(limit: 50, cursor: nil)
|
|
466
|
+
get("/api/v1/audit-log", { limit: limit, cursor: cursor })
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# ------------------------------------------------------------------
|
|
470
|
+
# Export
|
|
471
|
+
# ------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
# Export events as CSV or JSON. Returns raw string.
|
|
474
|
+
def export_events(format: "csv")
|
|
475
|
+
request_raw("GET", "/api/v1/export/events", params: { format: format })
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Export deliveries as CSV or JSON. Returns raw string.
|
|
479
|
+
def export_deliveries(format: "csv")
|
|
480
|
+
request_raw("GET", "/api/v1/export/deliveries", params: { format: format })
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Export jobs as CSV or JSON. Returns raw string.
|
|
484
|
+
def export_jobs(format: "csv")
|
|
485
|
+
request_raw("GET", "/api/v1/export/jobs", params: { format: format })
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Export audit log as CSV or JSON. Returns raw string.
|
|
489
|
+
def export_audit_log(format: "csv")
|
|
490
|
+
request_raw("GET", "/api/v1/export/audit-log", params: { format: format })
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# ------------------------------------------------------------------
|
|
494
|
+
# GDPR
|
|
495
|
+
# ------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
# Get current user consent status.
|
|
498
|
+
def get_consents
|
|
499
|
+
get("/api/v1/me/consents")
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Accept consent for a specific purpose.
|
|
503
|
+
def accept_consent(purpose)
|
|
504
|
+
post("/api/v1/me/consents/#{purpose}/accept", {})
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Export all personal data (GDPR data portability).
|
|
508
|
+
def export_my_data
|
|
509
|
+
get("/api/v1/me/data")
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Request restriction of data processing.
|
|
513
|
+
def restrict_processing
|
|
514
|
+
post("/api/v1/me/restrict", {})
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Lift restriction on data processing.
|
|
518
|
+
def lift_restriction
|
|
519
|
+
do_delete("/api/v1/me/restrict")
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Object to data processing.
|
|
523
|
+
def object_to_processing
|
|
524
|
+
post("/api/v1/me/object", {})
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Withdraw objection to data processing.
|
|
528
|
+
def restore_consent
|
|
529
|
+
do_delete("/api/v1/me/object")
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# ------------------------------------------------------------------
|
|
533
|
+
# Health
|
|
534
|
+
# ------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
# Check API health.
|
|
537
|
+
def health
|
|
538
|
+
get("/health")
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Get platform status page.
|
|
542
|
+
def status
|
|
543
|
+
get("/status")
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
private
|
|
547
|
+
|
|
548
|
+
# ------------------------------------------------------------------
|
|
549
|
+
# HTTP helpers
|
|
550
|
+
# ------------------------------------------------------------------
|
|
551
|
+
|
|
552
|
+
def get(path, params = nil)
|
|
553
|
+
request("GET", path, params: params)
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def post(path, body)
|
|
557
|
+
request("POST", path, body: body)
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def patch(path, body)
|
|
561
|
+
request("PATCH", path, body: body)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def do_delete(path)
|
|
565
|
+
request("DELETE", path)
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def public_post(path, body)
|
|
569
|
+
uri = build_uri(path)
|
|
570
|
+
http = build_http(uri)
|
|
571
|
+
|
|
572
|
+
req = Net::HTTP::Post.new(uri)
|
|
573
|
+
req["Content-Type"] = "application/json"
|
|
574
|
+
req.body = JSON.generate(body)
|
|
575
|
+
|
|
576
|
+
resp = http.request(req)
|
|
577
|
+
handle_response(resp)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def request(method, path, params: nil, body: nil)
|
|
581
|
+
uri = build_uri(path, params)
|
|
582
|
+
http = build_http(uri)
|
|
583
|
+
|
|
584
|
+
req = build_request(method, uri)
|
|
585
|
+
req["Content-Type"] = "application/json"
|
|
586
|
+
req["X-Api-Key"] = @api_key
|
|
587
|
+
req["Authorization"] = "Bearer #{@auth_token}" if @auth_token
|
|
588
|
+
req.body = JSON.generate(body) if body
|
|
589
|
+
|
|
590
|
+
resp = http.request(req)
|
|
591
|
+
handle_response(resp)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def request_raw(method, path, params: nil)
|
|
595
|
+
uri = build_uri(path, params)
|
|
596
|
+
http = build_http(uri)
|
|
597
|
+
|
|
598
|
+
req = build_request(method, uri)
|
|
599
|
+
req["Content-Type"] = "application/json"
|
|
600
|
+
req["X-Api-Key"] = @api_key
|
|
601
|
+
req["Authorization"] = "Bearer #{@auth_token}" if @auth_token
|
|
602
|
+
|
|
603
|
+
resp = http.request(req)
|
|
604
|
+
|
|
605
|
+
unless resp.is_a?(Net::HTTPSuccess)
|
|
606
|
+
detail = begin
|
|
607
|
+
parsed = JSON.parse(resp.body)
|
|
608
|
+
parsed.is_a?(Hash) ? (parsed["error"] || parsed) : parsed
|
|
609
|
+
rescue JSON::ParserError
|
|
610
|
+
resp.body
|
|
611
|
+
end
|
|
612
|
+
raise Jobcelis::Error.new(resp.code.to_i, detail)
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
resp.body
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def build_uri(path, params = nil)
|
|
619
|
+
uri = URI("#{@base_url}#{path}")
|
|
620
|
+
if params
|
|
621
|
+
cleaned = params.reject { |_, v| v.nil? }
|
|
622
|
+
uri.query = URI.encode_www_form(cleaned) unless cleaned.empty?
|
|
623
|
+
end
|
|
624
|
+
uri
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def build_http(uri)
|
|
628
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
629
|
+
http.use_ssl = (uri.scheme == "https")
|
|
630
|
+
http.open_timeout = @timeout
|
|
631
|
+
http.read_timeout = @timeout
|
|
632
|
+
http
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def build_request(method, uri)
|
|
636
|
+
request_path = uri.request_uri
|
|
637
|
+
case method.upcase
|
|
638
|
+
when "GET" then Net::HTTP::Get.new(request_path)
|
|
639
|
+
when "POST" then Net::HTTP::Post.new(request_path)
|
|
640
|
+
when "PATCH" then Net::HTTP::Patch.new(request_path)
|
|
641
|
+
when "DELETE" then Net::HTTP::Delete.new(request_path)
|
|
642
|
+
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def handle_response(resp)
|
|
647
|
+
unless resp.is_a?(Net::HTTPSuccess)
|
|
648
|
+
detail = begin
|
|
649
|
+
parsed = JSON.parse(resp.body)
|
|
650
|
+
parsed.is_a?(Hash) ? (parsed["error"] || parsed) : parsed
|
|
651
|
+
rescue JSON::ParserError
|
|
652
|
+
resp.body
|
|
653
|
+
end
|
|
654
|
+
raise Jobcelis::Error.new(resp.code.to_i, detail)
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
return nil if resp.code == "204"
|
|
658
|
+
|
|
659
|
+
JSON.parse(resp.body)
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Jobcelis
|
|
6
|
+
module WebhookVerifier
|
|
7
|
+
# Verify a webhook signature using HMAC-SHA256.
|
|
8
|
+
#
|
|
9
|
+
# @param secret [String] The webhook signing secret.
|
|
10
|
+
# @param body [String] The raw request body.
|
|
11
|
+
# @param signature [String] The signature from the X-Signature header.
|
|
12
|
+
# @return [Boolean] true if the signature is valid.
|
|
13
|
+
def self.verify(secret:, body:, signature:)
|
|
14
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
|
|
15
|
+
return false if signature.nil? || signature.empty?
|
|
16
|
+
|
|
17
|
+
OpenSSL.secure_compare(expected, signature)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/jobcelis.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jobcelis
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jobcelis
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: net-http
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
description: Ruby client for the Jobcelis API — events, webhooks, jobs, pipelines,
|
|
28
|
+
and more. Connects to https://jobcelis.com by default.
|
|
29
|
+
email: vladiceli6@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- lib/jobcelis.rb
|
|
37
|
+
- lib/jobcelis/client.rb
|
|
38
|
+
- lib/jobcelis/error.rb
|
|
39
|
+
- lib/jobcelis/webhook_verifier.rb
|
|
40
|
+
homepage: https://jobcelis.com
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
43
|
+
metadata:
|
|
44
|
+
homepage_uri: https://jobcelis.com
|
|
45
|
+
source_code_uri: https://github.com/vladimirCeli/jobcelis-ruby
|
|
46
|
+
documentation_uri: https://jobcelis.com/docs
|
|
47
|
+
post_install_message:
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '3.0'
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 3.5.22
|
|
63
|
+
signing_key:
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: Official Ruby SDK for the Jobcelis Event Infrastructure Platform
|
|
66
|
+
test_files: []
|