seatlayer 0.2.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 +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +40 -14
- data/lib/seatlayer/channels.rb +45 -7
- data/lib/seatlayer/http_client.rb +38 -20
- data/lib/seatlayer/inventory.rb +62 -22
- data/lib/seatlayer/resources.rb +65 -16
- data/lib/seatlayer/version.rb +1 -1
- data/lib/seatlayer.rb +6 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07a3046d14b181d770f7dddf673476c7b0d695d31d725adc042555afa145c144
|
|
4
|
+
data.tar.gz: 5923f05115c9741d3ab20ac8598616351eeac43d74a91b5da0c590275f0105b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9b16c52a2dad95aab45a2d0d75dad7be100ddb60497b554ce9adc1a86dacdab4e8a6dd273314ba67dd93a7156eebb5984550ebf9a5ad8c2cdc519a356ccf70a
|
|
7
|
+
data.tar.gz: 0fbe576e636045d4cdfc6ff3cffcbd16403fc3e595913832d352bdcebe66cc03fc128dcc2ab7bca3fc490558e099c77838a4fe6a88d54f66305c9a7928ea0ea8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
|
|
3
23
|
## 0.2.0 — 2026-08-12
|
|
4
24
|
|
|
5
25
|
- Added the `channels` resource for allocation management, access previews,
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# SeatLayer Ruby SDK
|
|
2
2
|
|
|
3
|
+
[](https://github.com/seatlayer/seatlayer-ruby/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/seatlayer)
|
|
5
|
+
[](https://www.ruby-lang.org/)
|
|
6
|
+
[](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
|
|
@@ -150,11 +158,9 @@ session = client.sessions.create_manage_session(
|
|
|
150
158
|
)
|
|
151
159
|
```
|
|
152
160
|
|
|
153
|
-
`capabilities` is **required** by this SDK even though the API defaults
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
payment gateway**. That is real money, moved by a token you handed to a browser; it should not
|
|
157
|
-
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.
|
|
158
164
|
|
|
159
165
|
The full set, all opt-in:
|
|
160
166
|
|
|
@@ -166,13 +172,27 @@ The full set, all opt-in:
|
|
|
166
172
|
| `event:reports` | Read sales and availability reports |
|
|
167
173
|
| `event:channels:view` | Read sales channels and their allocations |
|
|
168
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 |
|
|
169
181
|
|
|
170
182
|
The two `event:channels:*` capabilities are **not** in the default — a token minted before sales
|
|
171
183
|
channels existed must not silently acquire channel authority — so ask for them explicitly if the
|
|
172
184
|
page manages channels.
|
|
173
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
|
+
|
|
174
189
|
## Webhooks
|
|
175
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
|
+
|
|
176
196
|
Verify every delivery against the **raw** body. Re-encoding a parsed Hash changes the bytes and
|
|
177
197
|
verification will fail.
|
|
178
198
|
|
|
@@ -231,15 +251,20 @@ error carries `status`, `code`, `body` and `request_id` — quote the request id
|
|
|
231
251
|
|
|
232
252
|
## Reliability
|
|
233
253
|
|
|
234
|
-
**Retries.** 429, 408 and 5xx
|
|
235
|
-
wins when the server sends it.
|
|
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.
|
|
236
258
|
|
|
237
|
-
**Idempotency.**
|
|
238
|
-
one
|
|
239
|
-
|
|
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.
|
|
240
265
|
|
|
241
266
|
```ruby
|
|
242
|
-
client.
|
|
267
|
+
client.events.create(chart_id: chart_id, idempotency_key: "provision-event-#{event_id}")
|
|
243
268
|
```
|
|
244
269
|
|
|
245
270
|
```ruby
|
|
@@ -252,7 +277,8 @@ SeatLayer::Client.new(
|
|
|
252
277
|
|
|
253
278
|
## Escape hatch
|
|
254
279
|
|
|
255
|
-
For surface this SDK does not wrap yet
|
|
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:
|
|
256
282
|
|
|
257
283
|
```ruby
|
|
258
284
|
client.request("POST", "/v1/events/ev_1/some-new-route", body: { "qty" => 2 })
|
|
@@ -263,9 +289,9 @@ client.request("POST", "/v1/events/ev_1/some-new-route", body: { "qty" => 2 })
|
|
|
263
289
|
| Resource | Methods |
|
|
264
290
|
| --- | --- |
|
|
265
291
|
| `charts` | `list` `list_all` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` |
|
|
266
|
-
| `events` | `list` `list_all` `create` `retrieve` `update` `delete` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `retrieve_report` `retrieve_log` |
|
|
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` |
|
|
267
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` |
|
|
268
|
-
| `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` |
|
|
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` |
|
|
269
295
|
| `sessions` | `create_manage_session` `revoke_manage_session` `create_designer_session` `revoke_designer_session` |
|
|
270
296
|
| `webhooks` | `list` `create` `update` `delete` `list_deliveries` |
|
|
271
297
|
| `workspaces` | `list` `create` `retrieve` `update` |
|
data/lib/seatlayer/channels.rb
CHANGED
|
@@ -65,26 +65,64 @@ module SeatLayer
|
|
|
65
65
|
# Explicit keywords document each security boundary carried by the token.
|
|
66
66
|
# rubocop:disable Metrics/ParameterLists
|
|
67
67
|
def create_buyer_access_session(event_key, include_public:, allowed_origin:, channel_ids: nil,
|
|
68
|
-
expires_in_seconds: nil, max_quantity:
|
|
69
|
-
partner_ref:
|
|
68
|
+
expires_in_seconds: nil, max_quantity: UNSET, buyer_ref: UNSET,
|
|
69
|
+
partner_ref: UNSET, client_request_id: UNSET, idempotency_key: nil)
|
|
70
70
|
body = compact({ "channelIds" => channel_ids, "includePublic" => include_public,
|
|
71
|
-
"allowedOrigin" => allowed_origin, "expiresInSeconds" => expires_in_seconds
|
|
72
|
-
|
|
73
|
-
|
|
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 }))
|
|
74
75
|
@client.post("/v1/events/#{encode(event_key)}/buyer-access-sessions", body,
|
|
75
76
|
idempotency_key: idempotency_key)
|
|
76
77
|
end
|
|
77
78
|
# rubocop:enable Metrics/ParameterLists
|
|
78
79
|
|
|
79
|
-
def list_buyer_access_sessions(event_key,
|
|
80
|
+
def list_buyer_access_sessions(event_key, limit: nil)
|
|
80
81
|
@client.get("/v1/events/#{encode(event_key)}/buyer-access-sessions",
|
|
81
|
-
compact({ "
|
|
82
|
+
compact({ "limit" => limit }))
|
|
82
83
|
end
|
|
83
84
|
|
|
84
85
|
def revoke_buyer_access_session(event_key, session_id)
|
|
85
86
|
@client.delete("/v1/events/#{encode(event_key)}/buyer-access-sessions/#{encode(session_id)}")
|
|
86
87
|
end
|
|
87
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
|
+
|
|
88
126
|
private
|
|
89
127
|
|
|
90
128
|
def path(event_key, suffix = "") = "/v1/events/#{encode(event_key)}/channels#{suffix}"
|
|
@@ -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,
|
|
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 =
|
|
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
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/seatlayer/inventory.rb
CHANGED
|
@@ -62,8 +62,13 @@ module SeatLayer
|
|
|
62
62
|
# the checkout window — invoiced sales, a phone order on hold. Releasing
|
|
63
63
|
# first hands the seats to whoever is racing for them in between. A hold that
|
|
64
64
|
# is gone, expired, or at its renewal cap answers 409 +cannot_extend+.
|
|
65
|
-
def extend_hold(event_key, hold_id, ttl_ms: nil
|
|
66
|
-
|
|
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)
|
|
67
72
|
end
|
|
68
73
|
|
|
69
74
|
# Authoritative items and prices. Charge from this, not the browser.
|
|
@@ -98,8 +103,9 @@ module SeatLayer
|
|
|
98
103
|
end
|
|
99
104
|
|
|
100
105
|
# Hold inventory back from sale (house seats, production holds).
|
|
101
|
-
def block(event_key, labels:)
|
|
102
|
-
@client.post(path(event_key, "/block"),
|
|
106
|
+
def block(event_key, labels:, release_at: nil)
|
|
107
|
+
@client.post(path(event_key, "/block"),
|
|
108
|
+
compact({ "labels" => labels, "releaseAt" => release_at }))
|
|
103
109
|
end
|
|
104
110
|
|
|
105
111
|
def unblock(event_key, labels:)
|
|
@@ -147,24 +153,29 @@ module SeatLayer
|
|
|
147
153
|
# The governing rule: the SDK mints tokens, widgets consume them. Your secret
|
|
148
154
|
# key never reaches a browser.
|
|
149
155
|
class Sessions < Resource
|
|
150
|
-
CAPABILITIES = [
|
|
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
|
|
151
162
|
|
|
152
163
|
# Mint a manage-session token for the control room.
|
|
153
164
|
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
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)
|
|
159
169
|
if capabilities.nil? || capabilities.empty?
|
|
160
170
|
raise ArgumentError,
|
|
161
|
-
|
|
162
|
-
'["event:view"]. Omitting it server-side grants event:cancel, ' \
|
|
163
|
-
"which can reverse paid bookings."
|
|
171
|
+
'capabilities is required: pass the smallest set the page needs, e.g. ["event:view"].'
|
|
164
172
|
end
|
|
173
|
+
unknown = capabilities - CAPABILITIES
|
|
174
|
+
raise ArgumentError, "unsupported manage capabilities: #{unknown.join(", ")}" unless unknown.empty?
|
|
165
175
|
|
|
166
176
|
body = compact({ "allowedOrigin" => allowed_origin, "capabilities" => capabilities,
|
|
167
|
-
"expiresInSeconds" => expires_in_seconds
|
|
177
|
+
"expiresInSeconds" => expires_in_seconds,
|
|
178
|
+
"workspaceId" => workspace_id })
|
|
168
179
|
@client.post("/v1/events/#{encode(event_key)}/manage-sessions", body)
|
|
169
180
|
end
|
|
170
181
|
|
|
@@ -174,13 +185,19 @@ module SeatLayer
|
|
|
174
185
|
|
|
175
186
|
# Mint a designer token so an organiser can edit a chart inside your own UI.
|
|
176
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
|
|
177
190
|
def create_designer_session(workspace_id:, chart_id:, allowed_origin:,
|
|
178
|
-
authority: nil,
|
|
191
|
+
authority: nil, can_publish: nil, mode: nil,
|
|
192
|
+
safe_mode_options: nil, features: nil, expires_in_seconds: nil)
|
|
179
193
|
body = compact({ "workspaceId" => workspace_id, "chartId" => chart_id,
|
|
180
194
|
"allowedOrigin" => allowed_origin, "authority" => authority,
|
|
181
|
-
"
|
|
195
|
+
"canPublish" => can_publish, "mode" => mode,
|
|
196
|
+
"safeModeOptions" => safe_mode_options, "features" => features,
|
|
197
|
+
"expiresInSeconds" => expires_in_seconds })
|
|
182
198
|
@client.post("/v1/designer/sessions", body)
|
|
183
199
|
end
|
|
200
|
+
# rubocop:enable Metrics/ParameterLists
|
|
184
201
|
|
|
185
202
|
def revoke_designer_session(session_id)
|
|
186
203
|
@client.delete("/v1/designer/sessions/#{encode(session_id)}")
|
|
@@ -189,15 +206,23 @@ module SeatLayer
|
|
|
189
206
|
|
|
190
207
|
# Manage webhook subscriptions. To VERIFY a delivery, see SeatLayer::Webhook.
|
|
191
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
|
+
|
|
192
214
|
def list
|
|
193
215
|
@client.get("/v1/webhooks")
|
|
194
216
|
end
|
|
195
217
|
|
|
196
218
|
def create(url:, events:)
|
|
219
|
+
validate_events!(events)
|
|
197
220
|
@client.post("/v1/webhooks", { "url" => url, "events" => events })
|
|
198
221
|
end
|
|
199
222
|
|
|
200
|
-
def update(webhook_id,
|
|
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 })
|
|
201
226
|
@client.patch("/v1/webhooks/#{encode(webhook_id)}", fields)
|
|
202
227
|
end
|
|
203
228
|
|
|
@@ -205,8 +230,20 @@ module SeatLayer
|
|
|
205
230
|
@client.delete("/v1/webhooks/#{encode(webhook_id)}")
|
|
206
231
|
end
|
|
207
232
|
|
|
208
|
-
def list_deliveries(webhook_id)
|
|
209
|
-
|
|
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"
|
|
210
247
|
end
|
|
211
248
|
end
|
|
212
249
|
|
|
@@ -216,9 +253,12 @@ module SeatLayer
|
|
|
216
253
|
@client.get("/v1/workspaces")
|
|
217
254
|
end
|
|
218
255
|
|
|
219
|
-
def create(name:, external_ref:
|
|
220
|
-
body =
|
|
221
|
-
|
|
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
|
+
)
|
|
222
262
|
end
|
|
223
263
|
|
|
224
264
|
def retrieve(workspace_id)
|
data/lib/seatlayer/resources.rb
CHANGED
|
@@ -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(
|
|
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
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
data/lib/seatlayer/version.rb
CHANGED
data/lib/seatlayer.rb
CHANGED
|
@@ -46,10 +46,12 @@ module SeatLayer
|
|
|
46
46
|
@http.get("/health/ready")
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
# Escape hatch for surface this SDK does not wrap yet.
|
|
50
|
-
#
|
|
51
|
-
def request(method, path, query: nil, body: nil,
|
|
52
|
-
|
|
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)
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
57
|
end
|