seatlayer 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6aa9b6c1e8985b0f261f5e0bf7743148a2fcf5dc3c9f9585b482d78e24b492a5
4
- data.tar.gz: 70b5fb6fd1f3eb7cbed9d80917a7e1ef56d212c1bafb924e97a961878e71e906
3
+ metadata.gz: 07a3046d14b181d770f7dddf673476c7b0d695d31d725adc042555afa145c144
4
+ data.tar.gz: 5923f05115c9741d3ab20ac8598616351eeac43d74a91b5da0c590275f0105b0
5
5
  SHA512:
6
- metadata.gz: 21ab307cdbc1676e6d463c3bb990ebe886f5d38906b988d129d1f5a722c4199ffeb80938b582e3d7b906fe287b9ea5a190dda5043200f27f569fc0c2d2aab7f1
7
- data.tar.gz: d4d261bb2e9240a49bf373fd9faa63896fac970f9fb7a9f5c7583becce1c26a445f45ff623115463e833587f0a58be1762990864dba1066139c384d62d85005e
6
+ metadata.gz: d9b16c52a2dad95aab45a2d0d75dad7be100ddb60497b554ce9adc1a86dacdab4e8a6dd273314ba67dd93a7156eebb5984550ebf9a5ad8c2cdc519a356ccf70a
7
+ data.tar.gz: 0fbe576e636045d4cdfc6ff3cffcbd16403fc3e595913832d352bdcebe66cc03fc128dcc2ab7bca3fc490558e099c77838a4fe6a88d54f66305c9a7928ea0ea8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - **Security/reliability:** Mutations now default to a single attempt. Automatic header-replay
6
+ retries are limited to chart create/copy, event create, and workspace create, preventing
7
+ transient failures from duplicating holds or best-available results and from issuing extra
8
+ show-once credentials.
9
+ - Aligned event, inventory, webhook, manage-session, and Designer requests with the generated
10
+ public contract: event metadata, chart-update acknowledgement, trusted hold extension,
11
+ scheduled block release, hold-TTL reset, webhook envelopes and delivery filters, and Designer
12
+ safe-mode/feature-policy fields are now represented directly.
13
+ - Removed unsupported `state` and `cursor` keywords from buyer-access-session listing; the route
14
+ supports only `limit`.
15
+ - Added deterministic transport-contract coverage for stable error-code fallback, HTTP status,
16
+ decoded body and `X-Request-ID` exposure, typed 429 `Retry-After` precedence, non-JSON gateway
17
+ failures, and single-attempt unsafe mutations.
18
+ - Reached all 71 public operation wrappers with raw event-poster upload/removal and the complete
19
+ hosted access-link lifecycle. One-time capability reveals remain single-attempt.
20
+ - Added chart copy/metadata overrides, event-log pagination filters, and explicit-null support for
21
+ buyer-session and workspace fields.
22
+
23
+ ## 0.2.0 — 2026-08-12
24
+
25
+ - Added the `channels` resource for allocation management, access previews,
26
+ channel reporting, and origin-bound buyer access sessions.
27
+ - Added inventory booking lifecycle reads and channel-aware hold/book options.
28
+ - Require a non-empty stable booking reference for booking, unbooking, and
29
+ booking-detail reads.
30
+ - Expanded registry, documentation, and agent-discovery links in the README.
31
+
3
32
  ## 0.1.0 — 2026-08-04
4
33
 
5
34
  First public release: RubyGems `seatlayer`.
data/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # SeatLayer Ruby SDK
2
2
 
3
+ [![CI](https://github.com/seatlayer/seatlayer-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/seatlayer/seatlayer-ruby/actions/workflows/ci.yml)
4
+ [![Gem](https://img.shields.io/gem/v/seatlayer.svg)](https://rubygems.org/gems/seatlayer)
5
+ [![Ruby](https://img.shields.io/badge/Ruby-%E2%89%A53.0-CC342D.svg)](https://www.ruby-lang.org/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-111827.svg)](LICENSE)
7
+
3
8
  Official Ruby server SDK for the [SeatLayer](https://seatlayer.io) reserved-seating API.
4
9
 
5
10
  > **Server-side only.** This gem authenticates with your secret key. Never load it anywhere a
@@ -38,6 +43,9 @@ held = client.inventory.hold_best_available(event["key"], qty: 4)
38
43
  client.inventory.book(event["key"], hold_id: held["holdId"], booking_ref: "order-8842")
39
44
  ```
40
45
 
46
+ Nullable event-create fields distinguish omission from an explicit reset: passing, for example,
47
+ `venue: nil` sends JSON `null`; leaving `venue` out sends no field.
48
+
41
49
  ## Test vs live
42
50
 
43
51
  Keys carry their own mode. `sk_test_…` keys can only touch test-mode events and `sk_live_…` only
@@ -74,6 +82,27 @@ client.inventory.book_best_available(event_key, qty: 2, booking_ref: "phone-1183
74
82
  client.inventory.box_office_book(event_key, labels: ["A-1", "A-2"], booking_ref: "comp-14")
75
83
  ```
76
84
 
85
+ ## Private and partner sales
86
+
87
+ Channels split event inventory into explicit allocations. A channel id is
88
+ reporting/routing metadata, not browser authority. Authenticate the buyer in
89
+ your backend and mint a short-lived token restricted to the event, origin, and
90
+ allowed allocations:
91
+
92
+ ```ruby
93
+ access = client.channels.create_buyer_access_session(
94
+ event_key,
95
+ channel_ids: ["chn_partner_a"],
96
+ include_public: false,
97
+ allowed_origin: "https://tickets.example"
98
+ )
99
+ # Return access["token"] to the in-memory buyerAccessTokenProvider only.
100
+ ```
101
+
102
+ Never log or persist the returned `bse_…` bearer. Allocation setup, previews,
103
+ pause/archive controls, audit-safe session listing, and channel reports are on
104
+ `client.channels`.
105
+
77
106
  ## Listing and pagination
78
107
 
79
108
  `list` returns one page plus a `nextCursor`. `list_all` pages for you and returns a lazy
@@ -129,11 +158,9 @@ session = client.sessions.create_manage_session(
129
158
  )
130
159
  ```
131
160
 
132
- `capabilities` is **required** by this SDK even though the API defaults it. Omit it at the API level
133
- and you get `event:view`, `event:block`, `event:cancel` and `event:reports` including
134
- `event:cancel`, which unbooks paid seats **and authorises refunds against the organiser's connected
135
- payment gateway**. That is real money, moved by a token you handed to a browser; it should not
136
- arrive by forgetting an argument. Grant the smallest set the page needs.
161
+ `capabilities` is **required** by this SDK even though the raw API safely defaults an omitted list
162
+ to view-only (`event:view`). Keeping the argument required makes browser authority visible at every
163
+ call site. Grant the smallest set the page needs.
137
164
 
138
165
  The full set, all opt-in:
139
166
 
@@ -145,13 +172,27 @@ The full set, all opt-in:
145
172
  | `event:reports` | Read sales and availability reports |
146
173
  | `event:channels:view` | Read sales channels and their allocations |
147
174
  | `event:channels:manage` | Create, pause and archive channels; rotate access links |
175
+ | `event:orders:read` | Read SeatLayer-managed orders |
176
+ | `event:refund` | Refund a SeatLayer-managed order |
177
+ | `event:tickets:send` | Send SeatLayer-managed tickets |
178
+ | `event:door:view` | Read the door list |
179
+ | `event:door:checkin` | Check tickets in and out |
180
+ | `event:boxoffice` | Use the managed box-office surface |
148
181
 
149
182
  The two `event:channels:*` capabilities are **not** in the default — a token minted before sales
150
183
  channels existed must not silently acquire channel authority — so ask for them explicitly if the
151
184
  page manages channels.
152
185
 
186
+ Designer minting returns the API envelope unchanged: read the token and effective safe-mode and
187
+ feature policy under `result["session"]`. Pass `safe_mode_options` only with `mode: "safe"`.
188
+
153
189
  ## Webhooks
154
190
 
191
+ Subscription responses use the wire envelopes exactly: `list` returns `{"subs" => [...]}`,
192
+ `create` returns `{"sub" => ..., "secret" => ...}` (the secret is shown once), and `update`
193
+ returns `{"sub" => ...}`. `SeatLayer::Webhooks::EVENT_NAMES` is the exact eight-name event set;
194
+ delivery history accepts `limit`, `status` (`"ok"` or `"failed"`), and `before`.
195
+
155
196
  Verify every delivery against the **raw** body. Re-encoding a parsed Hash changes the bytes and
156
197
  verification will fail.
157
198
 
@@ -210,15 +251,20 @@ error carries `status`, `code`, `body` and `request_id` — quote the request id
210
251
 
211
252
  ## Reliability
212
253
 
213
- **Retries.** 429, 408 and 5xx are retried with exponential backoff and full jitter; `Retry-After`
214
- wins when the server sends it. 4xx is never retried it will not start succeeding.
254
+ **Retries.** Reads (`GET`/`HEAD`) retry 429, 408 and 5xx with exponential backoff and full jitter;
255
+ `Retry-After` wins when the server sends it. Automatic mutation retries are limited to the four
256
+ operations backed by exact response replay: `charts.create`, `charts.copy`, `events.create`, and
257
+ `workspaces.create`. Other 4xx responses are never retried.
215
258
 
216
- **Idempotency.** Every mutating request carries an `Idempotency-Key`, generated if you do not supply
217
- one, and **reused across retries** so a retried booking cannot become two bookings. Pass your own
218
- order id for end-to-end deduplication:
259
+ **Idempotency.** Those four replay-backed operations carry an `Idempotency-Key`, generated when you
260
+ do not supply one and reused across attempts. Other mutations are single-attempt and receive no
261
+ automatic key. A caller-supplied key is forwarded but does not enable retries. This includes
262
+ inventory holds and bookings, show-once credential or secret creation, unsupported operations, and
263
+ raw `request` mutations. Keep `booking_ref` in the booking body for reconciliation, but handle an
264
+ unknown network outcome explicitly instead of automatically repeating the sale.
219
265
 
220
266
  ```ruby
221
- client.inventory.book(event_key, hold_id: hold_id, idempotency_key: "order-#{order_id}")
267
+ client.events.create(chart_id: chart_id, idempotency_key: "provision-event-#{event_id}")
222
268
  ```
223
269
 
224
270
  ```ruby
@@ -231,7 +277,8 @@ SeatLayer::Client.new(
231
277
 
232
278
  ## Escape hatch
233
279
 
234
- For surface this SDK does not wrap yet same auth, retries, idempotency and error mapping:
280
+ For surface this SDK does not wrap yet, `request` keeps auth and error mapping. Raw reads retain the
281
+ read retry policy; raw mutations are always single-attempt because their replay contract is unknown:
235
282
 
236
283
  ```ruby
237
284
  client.request("POST", "/v1/events/ev_1/some-new-route", body: { "qty" => 2 })
@@ -242,8 +289,9 @@ client.request("POST", "/v1/events/ev_1/some-new-route", body: { "qty" => 2 })
242
289
  | Resource | Methods |
243
290
  | --- | --- |
244
291
  | `charts` | `list` `list_all` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` |
245
- | `events` | `list` `list_all` `create` `retrieve` `update` `delete` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `retrieve_report` `retrieve_log` |
246
- | `inventory` | `hold` `hold_best_available` `book_best_available` `extend_hold` `retrieve_hold` `release` `book` `box_office_book` `unbook` `block` `unblock` `unblock_all` `retrieve_availability` `update_availability` |
292
+ | `events` | `list` `list_all` `create` `retrieve` `update` `delete` `update_poster` `delete_poster` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `retrieve_report` `retrieve_log` |
293
+ | `inventory` | `hold` `hold_best_available` `book_best_available` `extend_hold` `retrieve_hold` `release` `book` `box_office_book` `unbook` `list_bookings` `retrieve_booking` `block` `unblock` `unblock_all` `retrieve_availability` `update_availability` |
294
+ | `channels` | `list_channels` `create_channel` `update_channel` `update_assignments` `list_allocation` `retrieve_access_preview` `retrieve_report` `pause` `unpause` `archive` `create_buyer_access_session` `list_buyer_access_sessions` `revoke_buyer_access_session` `create_access_link` `list_access_links` `rotate_access_link` `revoke_access_link` |
247
295
  | `sessions` | `create_manage_session` `revoke_manage_session` `create_designer_session` `revoke_designer_session` |
248
296
  | `webhooks` | `list` `create` `update` `delete` `list_deliveries` |
249
297
  | `workspaces` | `list` `create` `retrieve` `update` |
@@ -273,24 +321,25 @@ the public manifest, not just from the wrapper.
273
321
  - [Webhook verification](https://docs.seatlayer.io/server-sdk/webhooks/)
274
322
  - [Server API reference](https://docs.seatlayer.io/server-api/events/)
275
323
  - [OpenAPI description](https://docs.seatlayer.io/openapi.json)
324
+ - [Agent-readable documentation](https://docs.seatlayer.io/llms.txt)
276
325
  - [SeatLayer GitHub organization](https://github.com/seatlayer)
277
326
 
278
327
  ### Other SeatLayer SDKs
279
328
 
280
329
  | Surface | Package |
281
330
  |---|---|
282
- | Browser (vanilla) | [`@seatlayer/js`](https://github.com/seatlayer/seatlayer-sdk) |
283
- | React | [`@seatlayer/react`](https://github.com/seatlayer/seatlayer-sdk) |
284
- | React Native | [`@seatlayer/react-native`](https://github.com/seatlayer/seatlayer-react-native) |
331
+ | Browser (vanilla) | [`@seatlayer/js`](https://www.npmjs.com/package/@seatlayer/js) |
332
+ | React | [`@seatlayer/react`](https://www.npmjs.com/package/@seatlayer/react) |
333
+ | React Native | [`@seatlayer/react-native`](https://www.npmjs.com/package/@seatlayer/react-native) |
285
334
  | iOS | [`seatlayer-ios`](https://github.com/seatlayer/seatlayer-ios) |
286
335
  | Android | [`seatlayer-android`](https://github.com/seatlayer/seatlayer-android) |
287
- | Flutter | [`seatlayer_flutter`](https://github.com/seatlayer/seatlayer-flutter) |
288
- | Node.js (server) | [`@seatlayer/server`](https://github.com/seatlayer/seatlayer-node) |
289
- | Python (server) | [`seatlayer`](https://github.com/seatlayer/seatlayer-python) |
290
- | PHP (server) | [`seatlayer/seatlayer-php`](https://github.com/seatlayer/seatlayer-php) |
291
- | Java (server) | [`io.seatlayer:seatlayer-java`](https://github.com/seatlayer/seatlayer-java) |
292
- | Go (server) | [`github.com/seatlayer/seatlayer-go`](https://github.com/seatlayer/seatlayer-go) |
293
- | .NET (server) | [`SeatLayer`](https://github.com/seatlayer/seatlayer-dotnet) |
336
+ | Flutter | [`seatlayer`](https://pub.dev/packages/seatlayer) |
337
+ | Node.js (server) | [`@seatlayer/server`](https://www.npmjs.com/package/@seatlayer/server) |
338
+ | Python (server) | [`seatlayer`](https://pypi.org/project/seatlayer/) |
339
+ | PHP (server) | [`seatlayer/seatlayer-php`](https://packagist.org/packages/seatlayer/seatlayer-php) |
340
+ | Java (server) | [`io.seatlayer:seatlayer-java`](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java/0.1.0) |
341
+ | Go (server) | [`github.com/seatlayer/seatlayer-go`](https://pkg.go.dev/github.com/seatlayer/seatlayer-go) |
342
+ | .NET (server) | [`SeatLayer`](https://www.nuget.org/packages/SeatLayer) |
294
343
 
295
344
  ## Development
296
345
 
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SeatLayer
4
+ # Private allocations, reporting, and origin-bound buyer access.
5
+ class Channels < Resource
6
+ def list_channels(event_key, include_archived: false)
7
+ query = include_archived ? { "includeArchived" => "1" } : nil
8
+ @client.get(path(event_key), query)
9
+ end
10
+
11
+ def create_channel(event_key, name:, color: nil, marker: nil, external_ref: nil,
12
+ access_intent: nil, reason: nil, idempotency_key: nil)
13
+ body = compact({ "name" => name, "color" => color, "marker" => marker,
14
+ "externalRef" => external_ref, "accessIntent" => access_intent,
15
+ "reason" => reason })
16
+ @client.post(path(event_key), body, idempotency_key: idempotency_key)
17
+ end
18
+
19
+ def update_channel(event_key, channel_id, name: nil, access_intent: nil,
20
+ acknowledge_live_access: nil, reason: nil)
21
+ body = compact({ "name" => name, "accessIntent" => access_intent,
22
+ "acknowledgeLiveAccess" => acknowledge_live_access, "reason" => reason })
23
+ @client.patch(path(event_key, "/#{encode(channel_id)}"), body)
24
+ end
25
+
26
+ def update_assignments(event_key, labels:, assignment_version:, target_channel_id: nil,
27
+ reason: nil, idempotency_key: nil)
28
+ body = compact({ "targetChannelId" => target_channel_id, "labels" => labels,
29
+ "assignmentVersion" => assignment_version, "reason" => reason })
30
+ body["targetChannelId"] = nil if target_channel_id.nil?
31
+ @client.post(path(event_key, "/assignments"), body, idempotency_key: idempotency_key)
32
+ end
33
+
34
+ def list_allocation(event_key, after_label: nil, limit: nil)
35
+ @client.get(path(event_key, "/allocation"), compact({ "afterLabel" => after_label, "limit" => limit }))
36
+ end
37
+
38
+ def retrieve_access_preview(event_key, channel_ids: nil, include_public: nil)
39
+ include_public_value = if include_public.nil?
40
+ nil
41
+ else
42
+ include_public ? "1" : "0"
43
+ end
44
+ query = compact({ "channelIds" => channel_ids&.join(","),
45
+ "includePublic" => include_public_value })
46
+ @client.get(path(event_key, "/preview"), query)
47
+ end
48
+
49
+ def retrieve_report(event_key) = @client.get(path(event_key, "/report"))
50
+
51
+ def pause(event_key, channel_id, reason: nil)
52
+ @client.post(path(event_key, "/#{encode(channel_id)}/pause"), compact({ "reason" => reason }))
53
+ end
54
+
55
+ def unpause(event_key, channel_id, reason: nil)
56
+ @client.post(path(event_key, "/#{encode(channel_id)}/unpause"), compact({ "reason" => reason }))
57
+ end
58
+
59
+ def archive(event_key, channel_id, destination:, reason: nil)
60
+ body = compact({ "reason" => reason })
61
+ body["destination"] = destination
62
+ @client.post(path(event_key, "/#{encode(channel_id)}/archive"), body)
63
+ end
64
+
65
+ # Explicit keywords document each security boundary carried by the token.
66
+ # rubocop:disable Metrics/ParameterLists
67
+ def create_buyer_access_session(event_key, include_public:, allowed_origin:, channel_ids: nil,
68
+ expires_in_seconds: nil, max_quantity: UNSET, buyer_ref: UNSET,
69
+ partner_ref: UNSET, client_request_id: UNSET, idempotency_key: nil)
70
+ body = compact({ "channelIds" => channel_ids, "includePublic" => include_public,
71
+ "allowedOrigin" => allowed_origin, "expiresInSeconds" => expires_in_seconds })
72
+ body.merge!(supplied({ "maxQuantity" => max_quantity, "buyerRef" => buyer_ref,
73
+ "partnerRef" => partner_ref,
74
+ "clientRequestId" => client_request_id }))
75
+ @client.post("/v1/events/#{encode(event_key)}/buyer-access-sessions", body,
76
+ idempotency_key: idempotency_key)
77
+ end
78
+ # rubocop:enable Metrics/ParameterLists
79
+
80
+ def list_buyer_access_sessions(event_key, limit: nil)
81
+ @client.get("/v1/events/#{encode(event_key)}/buyer-access-sessions",
82
+ compact({ "limit" => limit }))
83
+ end
84
+
85
+ def revoke_buyer_access_session(event_key, session_id)
86
+ @client.delete("/v1/events/#{encode(event_key)}/buyer-access-sessions/#{encode(session_id)}")
87
+ end
88
+
89
+ # The URL and capability in this response are revealed once. Persist them
90
+ # immediately; this mutation is deliberately never retried automatically.
91
+ # rubocop:disable Metrics/ParameterLists
92
+ def create_access_link(event_key, channel_id, label: UNSET, expires_at: nil,
93
+ max_redemptions: nil, max_quantity: nil,
94
+ session_ttl_seconds: nil, include_public: nil, reason: nil,
95
+ idempotency_key: nil)
96
+ body = compact({ "expiresAt" => expires_at, "maxRedemptions" => max_redemptions,
97
+ "maxQuantity" => max_quantity,
98
+ "sessionTtlSeconds" => session_ttl_seconds,
99
+ "includePublic" => include_public, "reason" => reason })
100
+ body.merge!(supplied({ "label" => label }))
101
+ @client.post(path(event_key, "/#{encode(channel_id)}/access-links"), body,
102
+ idempotency_key: idempotency_key)
103
+ end
104
+ # rubocop:enable Metrics/ParameterLists
105
+
106
+ # Status only: the API never returns a previously revealed capability.
107
+ def list_access_links(event_key, channel_id)
108
+ @client.get(path(event_key, "/#{encode(channel_id)}/access-links"))
109
+ end
110
+
111
+ def rotate_access_link(event_key, channel_id, link_id, end_active_sessions:, reason: nil)
112
+ body = compact({ "endActiveSessions" => end_active_sessions, "reason" => reason })
113
+ @client.post(
114
+ path(event_key, "/#{encode(channel_id)}/access-links/#{encode(link_id)}/rotate"), body
115
+ )
116
+ end
117
+
118
+ def revoke_access_link(event_key, channel_id, link_id, end_active_sessions: false, reason: nil)
119
+ query = compact({ "endActiveSessions" => end_active_sessions ? "1" : nil,
120
+ "reason" => reason })
121
+ @client.delete(
122
+ path(event_key, "/#{encode(channel_id)}/access-links/#{encode(link_id)}"), query
123
+ )
124
+ end
125
+
126
+ private
127
+
128
+ def path(event_key, suffix = "") = "/v1/events/#{encode(event_key)}/channels#{suffix}"
129
+ end
130
+ end
@@ -62,7 +62,8 @@ module SeatLayer
62
62
  URI.encode_www_form_component(segment.to_s).gsub("+", "%20")
63
63
  end
64
64
 
65
- def request(method, path, query: nil, body: nil, idempotency_key: nil)
65
+ def request(method, path, query: nil, body: nil, raw_body: nil, content_type: nil,
66
+ idempotency_key: nil, retry_policy: :none)
66
67
  url = @base_url + path
67
68
  if query
68
69
  pairs = query.compact
@@ -74,56 +75,73 @@ module SeatLayer
74
75
  "Accept" => "application/json",
75
76
  "User-Agent" => "seatlayer-ruby"
76
77
  }
77
- payload = nil
78
- if body
79
- payload = JSON.generate(body)
80
- headers["Content-Type"] = "application/json"
81
- end
78
+ payload = build_payload(body, raw_body, content_type, headers)
82
79
 
83
- # Every mutation carries one. A retried POST that creates a second hold is
84
- # worse than a failed POST, and the caller cannot tell from outside so
85
- # the SDK, which knows it retried, is the right place to guarantee it.
80
+ # Only operations with exact server-side response replay get an automatic
81
+ # key. A caller key on any other mutation is forwarded, but cannot opt that
82
+ # operation into automatic retries.
86
83
  unless %w[GET HEAD].include?(method)
87
- key = idempotency_key || SecureRandom.uuid
88
- self.class.validate_idempotency_key!(key)
89
- headers["Idempotency-Key"] = key
84
+ key = idempotency_key
85
+ key ||= SecureRandom.uuid if retry_policy == :header_replay
86
+ if key
87
+ self.class.validate_idempotency_key!(key)
88
+ headers["Idempotency-Key"] = key
89
+ end
90
90
  end
91
91
 
92
- execute(method, url, headers, payload)
92
+ retry_allowed = %w[GET HEAD].include?(method) || retry_policy == :header_replay
93
+ execute(method, url, headers, payload, retry_allowed: retry_allowed)
93
94
  end
94
95
 
95
96
  def get(path, query = nil)
96
97
  request("GET", path, query: query)
97
98
  end
98
99
 
99
- def post(path, body = nil, idempotency_key: nil)
100
- request("POST", path, body: body, idempotency_key: idempotency_key)
100
+ def post(path, body = nil, idempotency_key: nil, retry_policy: :none)
101
+ request("POST", path, body: body, idempotency_key: idempotency_key, retry_policy: retry_policy)
101
102
  end
102
103
 
103
104
  def put(path, body)
104
105
  request("PUT", path, body: body)
105
106
  end
106
107
 
108
+ def put_raw(path, raw_body, content_type: "application/octet-stream")
109
+ request("PUT", path, raw_body: raw_body, content_type: content_type)
110
+ end
111
+
107
112
  def patch(path, body)
108
113
  request("PATCH", path, body: body)
109
114
  end
110
115
 
111
- def delete(path)
112
- request("DELETE", path)
116
+ def delete(path, query = nil)
117
+ request("DELETE", path, query: query)
113
118
  end
114
119
 
115
120
  private
116
121
 
122
+ def build_payload(body, raw_body, content_type, headers)
123
+ raise ArgumentError, "body and raw_body are mutually exclusive" if !body.nil? && !raw_body.nil?
124
+
125
+ unless raw_body.nil?
126
+ headers["Content-Type"] = content_type || "application/octet-stream"
127
+ return raw_body
128
+ end
129
+ return if body.nil?
130
+
131
+ headers["Content-Type"] = "application/json"
132
+ JSON.generate(body)
133
+ end
134
+
117
135
  # The retry loop, extracted from #request so each piece stays readable: the
118
136
  # public method builds the call, this one decides how many times to make it.
119
- def execute(method, url, headers, payload)
137
+ def execute(method, url, headers, payload, retry_allowed:)
120
138
  last_error = nil
121
139
 
122
140
  @max_retries.times do |attempt|
123
141
  begin
124
142
  response = send_request(method, url, headers, payload)
125
143
  rescue ConnectionError => e
126
- raise e if attempt >= @max_retries - 1
144
+ raise e unless retry_allowed && attempt < @max_retries - 1
127
145
 
128
146
  last_error = e
129
147
  sleep(backoff(attempt, nil))
@@ -136,7 +154,7 @@ module SeatLayer
136
154
  error_body = decode_error_body(response[:body])
137
155
  retry_after = parse_retry_after(response[:headers], error_body)
138
156
 
139
- if retryable?(status) && attempt < @max_retries - 1
157
+ if retry_allowed && retryable?(status) && attempt < @max_retries - 1
140
158
  sleep(backoff(attempt, status == 429 ? retry_after : nil))
141
159
  next
142
160
  end
@@ -11,10 +11,16 @@ module SeatLayer
11
11
  # Never price from what the browser tells you. +retrieve_hold+ is the
12
12
  # authoritative answer, which is why it is a separate call.
13
13
  class Inventory < Resource
14
+ # Explicit keywords keep allocation authority visible at every sale call.
15
+ # rubocop:disable Metrics/ParameterLists
14
16
  def hold(event_key, labels: nil, selections: nil, ttl_ms: nil,
15
- replace_hold_id: nil, idempotency_key: nil)
17
+ replace_hold_id: nil, channel_ids: nil, ignore_channel_restrictions: nil,
18
+ reason: nil, idempotency_key: nil)
16
19
  body = compact({ "labels" => labels, "selections" => selections,
17
- "ttlMs" => ttl_ms, "replaceHoldId" => replace_hold_id })
20
+ "ttlMs" => ttl_ms, "replaceHoldId" => replace_hold_id,
21
+ "channelIds" => channel_ids,
22
+ "ignoreChannelRestrictions" => ignore_channel_restrictions,
23
+ "reason" => reason })
18
24
  @client.post(path(event_key, "/hold"), body, idempotency_key: idempotency_key)
19
25
  end
20
26
 
@@ -24,9 +30,13 @@ module SeatLayer
24
30
  # order get the same answer for the same inventory. A +qty+ above the server
25
31
  # cap is clamped, not rejected.
26
32
  def hold_best_available(event_key, qty:, category_key: nil, zone_id: nil,
27
- ttl_ms: nil, idempotency_key: nil)
33
+ ttl_ms: nil, channel_ids: nil, ignore_channel_restrictions: nil,
34
+ reason: nil, idempotency_key: nil)
28
35
  body = compact({ "qty" => qty, "categoryKey" => category_key,
29
- "zoneId" => zone_id, "ttlMs" => ttl_ms })
36
+ "zoneId" => zone_id, "ttlMs" => ttl_ms,
37
+ "channelIds" => channel_ids,
38
+ "ignoreChannelRestrictions" => ignore_channel_restrictions,
39
+ "reason" => reason })
30
40
  @client.post(path(event_key, "/best-available"), body, idempotency_key: idempotency_key)
31
41
  end
32
42
 
@@ -35,11 +45,16 @@ module SeatLayer
35
45
  # Prefer this over hold-then-book when payment is already taken: a failure
36
46
  # between two calls would strand inventory until the TTL expired.
37
47
  def book_best_available(event_key, qty:, booking_ref:, category_key: nil,
38
- zone_id: nil, idempotency_key: nil)
39
- body = compact({ "qty" => qty, "bookingRef" => booking_ref,
40
- "categoryKey" => category_key, "zoneId" => zone_id })
48
+ zone_id: nil, channel_ids: nil, ignore_channel_restrictions: nil,
49
+ reason: nil, idempotency_key: nil)
50
+ body = compact({ "qty" => qty, "bookingRef" => normalise_booking_ref(booking_ref),
51
+ "categoryKey" => category_key, "zoneId" => zone_id,
52
+ "channelIds" => channel_ids,
53
+ "ignoreChannelRestrictions" => ignore_channel_restrictions,
54
+ "reason" => reason })
41
55
  @client.post(path(event_key, "/best-available-book"), body, idempotency_key: idempotency_key)
42
56
  end
57
+ # rubocop:enable Metrics/ParameterLists
43
58
 
44
59
  # Push an active hold's expiry out by a fresh window before it lapses.
45
60
  #
@@ -47,8 +62,13 @@ module SeatLayer
47
62
  # the checkout window — invoiced sales, a phone order on hold. Releasing
48
63
  # first hands the seats to whoever is racing for them in between. A hold that
49
64
  # is gone, expired, or at its renewal cap answers 409 +cannot_extend+.
50
- def extend_hold(event_key, hold_id, ttl_ms: nil)
51
- @client.post(path(event_key, "/extend"), compact({ "holdId" => hold_id, "ttlMs" => ttl_ms }))
65
+ def extend_hold(event_key, hold_id, ttl_ms: nil, channel_ids: nil,
66
+ ignore_channel_restrictions: nil, reason: nil)
67
+ body = compact({ "holdId" => hold_id, "ttlMs" => ttl_ms,
68
+ "channelIds" => channel_ids,
69
+ "ignoreChannelRestrictions" => ignore_channel_restrictions,
70
+ "reason" => reason })
71
+ @client.post(path(event_key, "/extend"), body)
52
72
  end
53
73
 
54
74
  # Authoritative items and prices. Charge from this, not the browser.
@@ -60,25 +80,32 @@ module SeatLayer
60
80
  @client.post(path(event_key, "/release"), { "labels" => labels, "holdId" => hold_id })
61
81
  end
62
82
 
63
- def book(event_key, hold_id: nil, labels: nil, booking_ref: nil, idempotency_key: nil)
64
- body = compact({ "holdId" => hold_id, "labels" => labels, "bookingRef" => booking_ref })
83
+ def book(event_key, hold_id: nil, labels: nil, booking_ref: nil, channel_ids: nil,
84
+ ignore_channel_restrictions: nil, reason: nil, idempotency_key: nil)
85
+ body = compact({ "holdId" => hold_id, "labels" => labels,
86
+ "bookingRef" => normalise_booking_ref(booking_ref),
87
+ "channelIds" => channel_ids,
88
+ "ignoreChannelRestrictions" => ignore_channel_restrictions,
89
+ "reason" => reason })
65
90
  @client.post(path(event_key, "/book"), body, idempotency_key: idempotency_key)
66
91
  end
67
92
 
68
93
  def box_office_book(event_key, labels:, booking_ref:, idempotency_key: nil)
69
94
  @client.post(path(event_key, "/box-book"),
70
- { "labels" => labels, "bookingRef" => booking_ref },
95
+ { "labels" => labels, "bookingRef" => normalise_booking_ref(booking_ref) },
71
96
  idempotency_key: idempotency_key)
72
97
  end
73
98
 
74
99
  # Reverse a booking. Requires a key with cancel authority.
75
- def unbook(event_key, labels:)
76
- @client.post(path(event_key, "/unbook"), { "labels" => labels })
100
+ def unbook(event_key, labels:, booking_ref:)
101
+ @client.post(path(event_key, "/unbook"),
102
+ { "labels" => labels, "bookingRef" => normalise_booking_ref(booking_ref) })
77
103
  end
78
104
 
79
105
  # Hold inventory back from sale (house seats, production holds).
80
- def block(event_key, labels:)
81
- @client.post(path(event_key, "/block"), { "labels" => labels })
106
+ def block(event_key, labels:, release_at: nil)
107
+ @client.post(path(event_key, "/block"),
108
+ compact({ "labels" => labels, "releaseAt" => release_at }))
82
109
  end
83
110
 
84
111
  def unblock(event_key, labels:)
@@ -97,11 +124,28 @@ module SeatLayer
97
124
  @client.post(path(event_key, "/availability"), fields)
98
125
  end
99
126
 
127
+ # One page of inventory booking lifecycles, newest first.
128
+ def list_bookings(event_key, query: nil, state: nil, limit: nil, cursor: nil)
129
+ @client.get(path(event_key, "/bookings"),
130
+ compact({ "q" => query, "state" => state, "limit" => limit, "cursor" => cursor }))
131
+ end
132
+
133
+ def retrieve_booking(event_key, booking_ref)
134
+ @client.get(path(event_key, "/bookings/#{encode(normalise_booking_ref(booking_ref))}"))
135
+ end
136
+
100
137
  private
101
138
 
102
139
  def path(event_key, suffix)
103
140
  "/v1/events/#{encode(event_key)}#{suffix}"
104
141
  end
142
+
143
+ def normalise_booking_ref(booking_ref)
144
+ value = booking_ref&.strip
145
+ return value unless value.nil? || value.empty?
146
+
147
+ raise ArgumentError, "booking_ref is required and must be a non-empty stable reference"
148
+ end
105
149
  end
106
150
 
107
151
  # Short-lived, origin-bound browser tokens.
@@ -109,24 +153,29 @@ module SeatLayer
109
153
  # The governing rule: the SDK mints tokens, widgets consume them. Your secret
110
154
  # key never reaches a browser.
111
155
  class Sessions < Resource
112
- CAPABILITIES = ["event:view", "event:block", "event:cancel", "event:reports"].freeze
156
+ CAPABILITIES = [
157
+ "event:view", "event:block", "event:cancel", "event:reports",
158
+ "event:channels:view", "event:channels:manage", "event:orders:read",
159
+ "event:refund", "event:tickets:send", "event:door:view",
160
+ "event:door:checkin", "event:boxoffice"
161
+ ].freeze
113
162
 
114
163
  # Mint a manage-session token for the control room.
115
164
  #
116
- # +capabilities+ is required here even though the API defaults it. That
117
- # default grants all four including event:cancel, which un-books paid
118
- # inventory. Granting the ability to reverse sales by forgetting an argument
119
- # is not a default worth inheriting.
120
- def create_manage_session(event_key, allowed_origin:, capabilities:, expires_in_seconds: nil)
165
+ # The raw API defaults an omitted list to view-only (+event:view+). This SDK
166
+ # still requires an explicit set so browser authority is visible at each call.
167
+ def create_manage_session(event_key, allowed_origin:, capabilities:, expires_in_seconds: nil,
168
+ workspace_id: nil)
121
169
  if capabilities.nil? || capabilities.empty?
122
170
  raise ArgumentError,
123
- "capabilities is required: pass the smallest set the page needs, e.g. " \
124
- '["event:view"]. Omitting it server-side grants event:cancel, ' \
125
- "which can reverse paid bookings."
171
+ 'capabilities is required: pass the smallest set the page needs, e.g. ["event:view"].'
126
172
  end
173
+ unknown = capabilities - CAPABILITIES
174
+ raise ArgumentError, "unsupported manage capabilities: #{unknown.join(", ")}" unless unknown.empty?
127
175
 
128
176
  body = compact({ "allowedOrigin" => allowed_origin, "capabilities" => capabilities,
129
- "expiresInSeconds" => expires_in_seconds })
177
+ "expiresInSeconds" => expires_in_seconds,
178
+ "workspaceId" => workspace_id })
130
179
  @client.post("/v1/events/#{encode(event_key)}/manage-sessions", body)
131
180
  end
132
181
 
@@ -136,13 +185,19 @@ module SeatLayer
136
185
 
137
186
  # Mint a designer token so an organiser can edit a chart inside your own UI.
138
187
  # Requires a chart id that already exists — create or copy one first.
188
+ # Explicit keywords keep each security and feature-policy boundary visible.
189
+ # rubocop:disable Metrics/ParameterLists
139
190
  def create_designer_session(workspace_id:, chart_id:, allowed_origin:,
140
- authority: nil, mode: nil, expires_in_seconds: nil)
191
+ authority: nil, can_publish: nil, mode: nil,
192
+ safe_mode_options: nil, features: nil, expires_in_seconds: nil)
141
193
  body = compact({ "workspaceId" => workspace_id, "chartId" => chart_id,
142
194
  "allowedOrigin" => allowed_origin, "authority" => authority,
143
- "mode" => mode, "expiresInSeconds" => expires_in_seconds })
195
+ "canPublish" => can_publish, "mode" => mode,
196
+ "safeModeOptions" => safe_mode_options, "features" => features,
197
+ "expiresInSeconds" => expires_in_seconds })
144
198
  @client.post("/v1/designer/sessions", body)
145
199
  end
200
+ # rubocop:enable Metrics/ParameterLists
146
201
 
147
202
  def revoke_designer_session(session_id)
148
203
  @client.delete("/v1/designer/sessions/#{encode(session_id)}")
@@ -151,15 +206,23 @@ module SeatLayer
151
206
 
152
207
  # Manage webhook subscriptions. To VERIFY a delivery, see SeatLayer::Webhook.
153
208
  class Webhooks < Resource
209
+ EVENT_NAMES = [
210
+ "seat.booked", "seat.released", "seat.blocked", "hold.expired",
211
+ "hold.created", "hold.extended", "event.created", "event.soldout"
212
+ ].freeze
213
+
154
214
  def list
155
215
  @client.get("/v1/webhooks")
156
216
  end
157
217
 
158
218
  def create(url:, events:)
219
+ validate_events!(events)
159
220
  @client.post("/v1/webhooks", { "url" => url, "events" => events })
160
221
  end
161
222
 
162
- def update(webhook_id, fields)
223
+ def update(webhook_id, url: nil, events: nil, disabled: nil)
224
+ validate_events!(events) unless events.nil?
225
+ fields = compact({ "url" => url, "events" => events, "disabled" => disabled })
163
226
  @client.patch("/v1/webhooks/#{encode(webhook_id)}", fields)
164
227
  end
165
228
 
@@ -167,8 +230,20 @@ module SeatLayer
167
230
  @client.delete("/v1/webhooks/#{encode(webhook_id)}")
168
231
  end
169
232
 
170
- def list_deliveries(webhook_id)
171
- @client.get("/v1/webhooks/#{encode(webhook_id)}/deliveries")
233
+ def list_deliveries(webhook_id, limit: nil, status: nil, before: nil)
234
+ raise ArgumentError, "status must be ok or failed" unless status.nil? || %w[ok failed].include?(status)
235
+
236
+ query = compact({ "limit" => limit, "status" => status, "before" => before })
237
+ @client.get("/v1/webhooks/#{encode(webhook_id)}/deliveries", query)
238
+ end
239
+
240
+ private
241
+
242
+ def validate_events!(events)
243
+ unknown = Array(events) - EVENT_NAMES
244
+ return if !Array(events).empty? && unknown.empty?
245
+
246
+ raise ArgumentError, "events must contain only supported SeatLayer webhook event names"
172
247
  end
173
248
  end
174
249
 
@@ -178,9 +253,12 @@ module SeatLayer
178
253
  @client.get("/v1/workspaces")
179
254
  end
180
255
 
181
- def create(name:, external_ref: nil, idempotency_key: nil)
182
- body = compact({ "name" => name, "externalRef" => external_ref })
183
- @client.post("/v1/workspaces", body, idempotency_key: idempotency_key)
256
+ def create(name:, external_ref: UNSET, idempotency_key: nil)
257
+ body = { "name" => name }
258
+ body.merge!(supplied({ "externalRef" => external_ref }))
259
+ @client.post(
260
+ "/v1/workspaces", body, idempotency_key: idempotency_key, retry_policy: :header_replay
261
+ )
184
262
  end
185
263
 
186
264
  def retrieve(workspace_id)
@@ -3,6 +3,8 @@
3
3
  module SeatLayer
4
4
  # Shared plumbing for the resource namespaces.
5
5
  class Resource
6
+ UNSET = Object.new.freeze
7
+
6
8
  def initialize(client)
7
9
  @client = client
8
10
  end
@@ -15,6 +17,11 @@ module SeatLayer
15
17
  hash.compact
16
18
  end
17
19
 
20
+ # Drop only the private sentinel, preserving nil as explicit JSON null.
21
+ def supplied(hash)
22
+ hash.reject { |_key, value| value.equal?(UNSET) }
23
+ end
24
+
18
25
  def encode(segment)
19
26
  HTTPClient.encode(segment)
20
27
  end
@@ -58,14 +65,16 @@ module SeatLayer
58
65
  def create(name:, doc: nil, external_ref: nil, workspace_id: nil, idempotency_key: nil)
59
66
  body = compact({ "name" => name, "doc" => doc,
60
67
  "externalRef" => external_ref, "workspaceId" => workspace_id })
61
- @client.post("/v1/charts", body, idempotency_key: idempotency_key)
68
+ @client.post(
69
+ "/v1/charts", body, idempotency_key: idempotency_key, retry_policy: :header_replay
70
+ )
62
71
  end
63
72
 
64
73
  def retrieve(chart_id)
65
74
  @client.get("/v1/charts/#{encode(chart_id)}")
66
75
  end
67
76
 
68
- # Replace a chart document.
77
+ # Replace a chart document or update metadata only.
69
78
  #
70
79
  # +expected_updated_at+ is required for optimistic concurrency and is not
71
80
  # optional here either: without it two concurrent writers silently overwrite
@@ -74,8 +83,20 @@ module SeatLayer
74
83
  #
75
84
  # The Designer is the authoring surface. Use this for bulk programmatic edits
76
85
  # and migrations, not for drawing.
77
- def update(chart_id, doc:, expected_updated_at:, name: nil)
78
- body = compact({ "doc" => doc, "expectedUpdatedAt" => expected_updated_at, "name" => name })
86
+ def update(chart_id, doc: UNSET, expected_updated_at: UNSET, name: nil, issues: nil,
87
+ external_ref: UNSET)
88
+ doc_supplied = !doc.equal?(UNSET)
89
+ expected_supplied = !expected_updated_at.equal?(UNSET)
90
+ unless doc_supplied == expected_supplied
91
+ raise ArgumentError, "doc and expected_updated_at must be supplied together"
92
+ end
93
+
94
+ body = compact({ "name" => name, "issues" => issues })
95
+ if doc_supplied
96
+ body["doc"] = doc
97
+ body["expectedUpdatedAt"] = expected_updated_at
98
+ end
99
+ body.merge!(supplied({ "externalRef" => external_ref }))
79
100
  @client.put("/v1/charts/#{encode(chart_id)}", body)
80
101
  end
81
102
 
@@ -84,8 +105,14 @@ module SeatLayer
84
105
  end
85
106
 
86
107
  # Copy a chart — the usual way to provision a venue from a template.
87
- def copy(chart_id, idempotency_key: nil)
88
- @client.post("/v1/charts/#{encode(chart_id)}/duplicate", nil, idempotency_key: idempotency_key)
108
+ def copy(chart_id, idempotency_key: nil, name: nil, external_ref: UNSET,
109
+ workspace_id: nil)
110
+ body = compact({ "name" => name, "workspaceId" => workspace_id })
111
+ body.merge!(supplied({ "externalRef" => external_ref }))
112
+ @client.post(
113
+ "/v1/charts/#{encode(chart_id)}/duplicate", body.empty? ? nil : body,
114
+ idempotency_key: idempotency_key, retry_policy: :header_replay
115
+ )
89
116
  end
90
117
 
91
118
  def archive(chart_id)
@@ -131,13 +158,22 @@ module SeatLayer
131
158
  end
132
159
  end
133
160
 
134
- def create(chart_id:, name: nil, slug: nil, starts_at: nil, venue: nil,
135
- external_ref: nil, currency: nil, idempotency_key: nil)
136
- body = compact({ "chartId" => chart_id, "name" => name, "slug" => slug,
137
- "startsAt" => starts_at, "venue" => venue,
138
- "externalRef" => external_ref, "currency" => currency })
139
- @client.post("/v1/events", body, idempotency_key: idempotency_key)
161
+ # rubocop:disable Metrics/ParameterLists
162
+ def create(chart_id:, name: nil, slug: nil, starts_at: UNSET, venue: UNSET,
163
+ external_ref: UNSET, currency: UNSET, idempotency_key: nil,
164
+ description: UNSET, ends_at: UNSET, timezone: UNSET, locale: UNSET,
165
+ poster_asset_id: UNSET, mode: nil)
166
+ body = compact({ "chartId" => chart_id, "name" => name, "slug" => slug, "mode" => mode })
167
+ body.merge!(supplied({ "startsAt" => starts_at, "venue" => venue,
168
+ "externalRef" => external_ref, "currency" => currency,
169
+ "description" => description, "endsAt" => ends_at,
170
+ "timezone" => timezone, "locale" => locale,
171
+ "posterAssetId" => poster_asset_id }))
172
+ @client.post(
173
+ "/v1/events", body, idempotency_key: idempotency_key, retry_policy: :header_replay
174
+ )
140
175
  end
176
+ # rubocop:enable Metrics/ParameterLists
141
177
 
142
178
  def retrieve(event_key)
143
179
  @client.get("/v1/events/#{encode(event_key)}")
@@ -151,9 +187,20 @@ module SeatLayer
151
187
  @client.delete("/v1/events/#{encode(event_key)}")
152
188
  end
153
189
 
190
+ # Upload raw PNG, JPEG, or WebP bytes (maximum 5 MiB).
191
+ def update_poster(event_key, image, content_type: "application/octet-stream")
192
+ @client.put_raw("/v1/events/#{encode(event_key)}/poster", image, content_type: content_type)
193
+ end
194
+
195
+ def delete_poster(event_key)
196
+ @client.delete("/v1/events/#{encode(event_key)}/poster")
197
+ end
198
+
154
199
  # Move a live event onto the latest published version of its chart.
155
- def update_chart(event_key)
156
- @client.post("/v1/events/#{encode(event_key)}/update-chart")
200
+ def update_chart(event_key, acknowledge_dropped_assignments: nil, reason: nil)
201
+ body = compact({ "acknowledgeDroppedAssignments" => acknowledge_dropped_assignments,
202
+ "reason" => reason })
203
+ @client.post("/v1/events/#{encode(event_key)}/update-chart", body)
157
204
  end
158
205
 
159
206
  # Stop buyer sales. Existing holds keep their TTL.
@@ -174,6 +221,7 @@ module SeatLayer
174
221
  end
175
222
 
176
223
  def update_hold_ttl(event_key, hold_ttl_ms)
224
+ # +nil+ restores the event default and must remain an explicit JSON null.
177
225
  @client.post("/v1/events/#{encode(event_key)}/hold-ttl", { "holdTtlMs" => hold_ttl_ms })
178
226
  end
179
227
 
@@ -181,8 +229,9 @@ module SeatLayer
181
229
  @client.get("/v1/events/#{encode(event_key)}/report")
182
230
  end
183
231
 
184
- def retrieve_log(event_key)
185
- @client.get("/v1/events/#{encode(event_key)}/log")
232
+ def retrieve_log(event_key, limit: nil, before: nil)
233
+ @client.get("/v1/events/#{encode(event_key)}/log",
234
+ compact({ "limit" => limit, "before" => before }))
186
235
  end
187
236
  end
188
237
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SeatLayer
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/seatlayer.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "seatlayer/errors"
5
5
  require_relative "seatlayer/http_client"
6
6
  require_relative "seatlayer/resources"
7
7
  require_relative "seatlayer/inventory"
8
+ require_relative "seatlayer/channels"
8
9
  require_relative "seatlayer/webhook"
9
10
 
10
11
  # Official Ruby server SDK for the SeatLayer reserved-seating API.
@@ -18,7 +19,7 @@ require_relative "seatlayer/webhook"
18
19
  module SeatLayer
19
20
  # The SeatLayer client.
20
21
  class Client
21
- attr_reader :charts, :events, :inventory, :sessions, :webhooks, :workspaces
22
+ attr_reader :charts, :events, :inventory, :channels, :sessions, :webhooks, :workspaces
22
23
 
23
24
  def initialize(secret_key, base_url: HTTPClient::DEFAULT_BASE_URL,
24
25
  max_retries: HTTPClient::DEFAULT_MAX_RETRIES,
@@ -29,6 +30,7 @@ module SeatLayer
29
30
  @charts = Charts.new(@http)
30
31
  @events = Events.new(@http)
31
32
  @inventory = Inventory.new(@http)
33
+ @channels = Channels.new(@http)
32
34
  @sessions = Sessions.new(@http)
33
35
  @webhooks = Webhooks.new(@http)
34
36
  @workspaces = Workspaces.new(@http)
@@ -44,10 +46,12 @@ module SeatLayer
44
46
  @http.get("/health/ready")
45
47
  end
46
48
 
47
- # Escape hatch for surface this SDK does not wrap yet. Carries the same auth,
48
- # retries, idempotency and error mapping.
49
- def request(method, path, query: nil, body: nil, idempotency_key: nil)
50
- @http.request(method, path, query: query, body: body, idempotency_key: idempotency_key)
49
+ # Escape hatch for surface this SDK does not wrap yet. Reads retain retries;
50
+ # raw mutations are single-attempt because their replay contract is unknown.
51
+ def request(method, path, query: nil, body: nil, raw_body: nil, content_type: nil,
52
+ idempotency_key: nil)
53
+ @http.request(method, path, query: query, body: body, raw_body: raw_body,
54
+ content_type: content_type, idempotency_key: idempotency_key)
51
55
  end
52
56
  end
53
57
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: seatlayer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SeatLayer
@@ -21,6 +21,7 @@ files:
21
21
  - LICENSE
22
22
  - README.md
23
23
  - lib/seatlayer.rb
24
+ - lib/seatlayer/channels.rb
24
25
  - lib/seatlayer/errors.rb
25
26
  - lib/seatlayer/http_client.rb
26
27
  - lib/seatlayer/inventory.rb