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 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jobcelis
4
+ class Error < StandardError
5
+ attr_reader :status, :detail
6
+
7
+ def initialize(status, detail)
8
+ @status = status
9
+ @detail = detail
10
+ super("HTTP #{status}: #{detail}")
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "jobcelis/error"
4
+ require_relative "jobcelis/webhook_verifier"
5
+ require_relative "jobcelis/client"
6
+
7
+ module Jobcelis
8
+ VERSION = "1.0.0"
9
+ end
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: []