cloudflare-ruby 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e840df4dc6ff74e2cb0fddaba160fc041c80472c52f7a583f9c23185d24b8765
4
- data.tar.gz: 9a42b0db13ed3d842fb1bc1b9df86011de55223a6667a666e3431bd455d4e4fd
3
+ metadata.gz: 230de80c474a3f8441f60687160aba0f2cd53c5db2d9465498866994150cc60f
4
+ data.tar.gz: 470768dc30305293361258880a74be07ea42832449ec4a77cae0079d42ca3cbc
5
5
  SHA512:
6
- metadata.gz: 71d24e6e2fbb63aee5c96e23e581f668207df71b7cc281cc02bfdb8c07e2e377e3a30e5a5888c90a5dd1acb0b0b4a4d734faf779bc9140b51a2400cad6029769
7
- data.tar.gz: 9a73874dadf0b7847b5058a4ab40dec0acedb93765438d0c2b96632c376bddf1a9b23f04ee167206c7ee35a138ca8cfabca8c2e6eba66a20bd646050e13f5c7a
6
+ metadata.gz: 3416c0f5b271e376182525b183324d5ec7e0c11e5437f13194a75f35265c8463ac6d843414386e4fb1954966c340515f6eee2540e10b67bec1d6b50eae98574b
7
+ data.tar.gz: 7c2d7a9a594d06f674af34d26cf9a9570da463ac81e817aa56738d2a0332760c0116550c3adf68b1616cd019f33b2e9ccd24dbcf8ab7e414ef3e222d57e5e68d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - **Per-product API tokens.** Set `Cloudflare::RealtimeKit.api_token` to
6
+ use a token scoped tightly to RealtimeKit (e.g., `Realtime: Edit` only).
7
+ Resolution chain when a request fires: per-call `api_token:` kwarg →
8
+ `Cloudflare::RealtimeKit.api_token` (per-product) → `Cloudflare.api_token`
9
+ (top-level fallback) → raise. Lets each product surface have its own
10
+ credentials so a leaked Realtime token can't touch R2/Workers/DNS.
11
+ - `Connection#request` accepts a new `api_token:` kwarg; resolved token
12
+ flows through one place.
13
+ - `Resource` gains a private `request(method, path, **opts)` helper
14
+ (class + instance) that wraps `Connection.instance.request` and injects
15
+ `configured_api_token` from the resource's product namespace. Subclasses
16
+ call `request(...)` instead of `Connection.instance.request(...)`.
17
+ - Top-level `Cloudflare.api_token` still works as the fallback for
18
+ callers who don't want per-product separation. Backward-compatible.
19
+
3
20
  ## 0.2.0
4
21
 
5
22
  - **Default `app_id` per process.** Set `Cloudflare::RealtimeKit.app_id`
@@ -8,11 +8,17 @@ module Cloudflare
8
8
  def reset! = @instance = nil
9
9
  end
10
10
 
11
- def request(method, path, body: nil, params: nil)
12
- raise Error, "Cloudflare.api_token not configured" if Cloudflare.api_token.nil?
11
+ # Sends a Cloudflare API request. The +api_token+ kwarg is the resolved
12
+ # token chosen by the caller (typically +Resource.configured_api_token+ —
13
+ # per-product token if set, top-level +Cloudflare.api_token+ otherwise).
14
+ # It's a kwarg rather than a hardcoded read so admin tools can override
15
+ # per-call.
16
+ def request(method, path, body: nil, params: nil, api_token: nil)
17
+ token = api_token || Cloudflare.api_token
18
+ raise Error, "Cloudflare API token not configured" if token.nil?
13
19
 
14
20
  faraday.public_send(method, path.delete_prefix("/")) do |req|
15
- req.headers["Authorization"] = "Bearer #{Cloudflare.api_token}"
21
+ req.headers["Authorization"] = "Bearer #{token}"
16
22
  req.headers["User-Agent"] = Cloudflare.configuration.user_agent
17
23
  req.params = compact(params) if params
18
24
  req.body = compact(body) if body
@@ -8,19 +8,21 @@ Ruby bindings for [Cloudflare RealtimeKit](https://developers.cloudflare.com/rea
8
8
  require "cloudflare"
9
9
 
10
10
  Cloudflare.configure do |c|
11
- c.api_token = ENV.fetch("CLOUDFLARE_API_TOKEN")
12
11
  c.account_id = ENV.fetch("CLOUDFLARE_ACCOUNT_ID")
13
12
  end
14
13
 
15
- # Default app_id for every RealtimeKit call this process makes.
16
- # Pass `app_id:` per-call only to override (e.g., admin tools that hop
17
- # between apps).
18
- Cloudflare::RealtimeKit.app_id = ENV.fetch("REALTIMEKIT_APP_ID")
14
+ # Per-product API token + default app_id for every RealtimeKit call this
15
+ # process makes. Pass `api_token:` / `app_id:` per-call only to override
16
+ # (e.g., admin tools that hop between accounts or apps).
17
+ Cloudflare::RealtimeKit.api_token = ENV.fetch("REALTIMEKIT_API_TOKEN")
18
+ Cloudflare::RealtimeKit.app_id = ENV.fetch("REALTIMEKIT_APP_ID")
19
19
 
20
20
  RK = Cloudflare::RealtimeKit # alias to taste
21
21
  ```
22
22
 
23
- After that, both `account_id:` and `app_id:` are implicit on every call — pass either per-call only to override.
23
+ After that, `account_id:`, `api_token:`, and `app_id:` are all implicit on every call — pass any per-call to override.
24
+
25
+ If you'd rather use a single token across every Cloudflare product (less scope-tight but simpler), set `Cloudflare.api_token` instead and skip the per-product line — every product surface falls back to it.
24
26
 
25
27
  ## Apps
26
28
 
@@ -36,31 +36,31 @@ module Cloudflare
36
36
 
37
37
  # POST .../active-session/kick — remove specified participants.
38
38
  def kick(participant_ids:, custom_participant_ids:)
39
- Connection.instance.request(:post, "#{member_path}/kick",
39
+ request(:post, "#{member_path}/kick",
40
40
  body: { participant_ids: participant_ids, custom_participant_ids: custom_participant_ids })
41
41
  end
42
42
 
43
43
  # POST .../active-session/kick-all — remove every participant.
44
44
  def kick_all
45
- Connection.instance.request(:post, "#{member_path}/kick-all")
45
+ request(:post, "#{member_path}/kick-all")
46
46
  end
47
47
 
48
48
  # POST .../active-session/mute — mute specified participants.
49
49
  def mute(participant_ids:, custom_participant_ids:)
50
- Connection.instance.request(:post, "#{member_path}/mute",
50
+ request(:post, "#{member_path}/mute",
51
51
  body: { participant_ids: participant_ids, custom_participant_ids: custom_participant_ids })
52
52
  end
53
53
 
54
54
  # POST .../active-session/mute-all — mute every participant.
55
55
  # +allow_unmute+ controls whether participants can re-enable their mic.
56
56
  def mute_all(allow_unmute:)
57
- Connection.instance.request(:post, "#{member_path}/mute-all",
57
+ request(:post, "#{member_path}/mute-all",
58
58
  body: { allow_unmute: allow_unmute })
59
59
  end
60
60
 
61
61
  # POST .../active-session/poll — broadcast a poll to participants.
62
62
  def create_poll(question:, options:, anonymous: nil, hide_votes: nil)
63
- Connection.instance.request(:post, "#{member_path}/poll",
63
+ request(:post, "#{member_path}/poll",
64
64
  body: { question: question, options: options, anonymous: anonymous, hide_votes: hide_votes })
65
65
  end
66
66
  end
@@ -35,7 +35,8 @@ module Cloudflare
35
35
  def fetch(path_template, app_id:, account_id:, params:)
36
36
  scope = build_scope(account_id: account_id, app_id: app_id)
37
37
  path = interpolate(path_template, scope)
38
- response = Connection.instance.request(:get, path, params: params)
38
+ response = Connection.instance.request(:get, path,
39
+ params: params, api_token: RealtimeKit.api_token || Cloudflare.api_token)
39
40
  Resource.unwrap_envelope(response)
40
41
  end
41
42
 
@@ -34,14 +34,14 @@ module Cloudflare
34
34
  # +{ data: [...] }+ envelope and don't need this dance.
35
35
  def create(name:, account_id: nil)
36
36
  scope = build_scope(account_id: account_id)
37
- response = Connection.instance.request(:post, interpolate(_collection_path, scope), body: { "name" => name })
37
+ response = request(:post, interpolate(_collection_path, scope), body: { "name" => name })
38
38
  new(unwrap_create_payload(response), scope: scope)
39
39
  end
40
40
 
41
41
  # GET /accounts/{account_id}/realtime/kit/apps
42
42
  def all(account_id: nil)
43
43
  scope = build_scope(account_id: account_id)
44
- response = Connection.instance.request(:get, interpolate(_collection_path, scope))
44
+ response = request(:get, interpolate(_collection_path, scope))
45
45
  Array(unwrap_envelope(response)).map { new(_1, scope: scope) }
46
46
  end
47
47
 
@@ -43,14 +43,14 @@ module Cloudflare
43
43
  # PUT /meetings/{id} — full replacement. Mirrors +update+ but uses PUT
44
44
  # semantics, so omitted fields revert to their defaults upstream.
45
45
  def replace(**attrs)
46
- response = Connection.instance.request(:put, member_path, body: self.class.to_wire_keys(attrs))
46
+ response = request(:put, member_path, body: self.class.to_wire_keys(attrs))
47
47
  set_attrs_from_response(response)
48
48
  self
49
49
  end
50
50
 
51
51
  # POST /meetings/{id}/livestreams — begin broadcasting this meeting.
52
52
  def start_livestreaming(name: nil, video_config: nil)
53
- Connection.instance.request(:post, "#{member_path}/livestreams", body: { name: name, video_config: video_config })
53
+ request(:post, "#{member_path}/livestreams", body: { name: name, video_config: video_config })
54
54
  end
55
55
 
56
56
  # GET /meetings/{id}/active-livestream — fetch the currently-running
@@ -58,13 +58,13 @@ module Cloudflare
58
58
  # the same app, so you can chain regular livestream operations on it
59
59
  # (e.g., +meeting.active_livestream.active_session+).
60
60
  def active_livestream
61
- response = Connection.instance.request(:get, "#{member_path}/active-livestream")
61
+ response = request(:get, "#{member_path}/active-livestream")
62
62
  Livestream.new(response, scope: { account_id: @scope[:account_id], app_id: @scope[:app_id] })
63
63
  end
64
64
 
65
65
  # POST /meetings/{id}/active-livestream/stop
66
66
  def stop_livestream
67
- Connection.instance.request(:post, "#{member_path}/active-livestream/stop")
67
+ request(:post, "#{member_path}/active-livestream/stop")
68
68
  end
69
69
 
70
70
  # GET /meetings/{id}/livestream — legacy "livestream-session-details"
@@ -73,7 +73,7 @@ module Cloudflare
73
73
  # Returned as a raw Hash because the response bundles two distinct
74
74
  # shapes; the typed +Livestream+ + per-session lookup live elsewhere.
75
75
  def livestream_details(page_no: nil, per_page: nil)
76
- response = Connection.instance.request(:get, "#{member_path}/livestream",
76
+ response = request(:get, "#{member_path}/livestream",
77
77
  params: { page_no: page_no, per_page: per_page })
78
78
  self.class.unwrap_envelope(response)
79
79
  end
@@ -31,7 +31,7 @@ module Cloudflare
31
31
  # Mints a fresh token for an existing participant (e.g., when their
32
32
  # previous token expired or was leaked).
33
33
  def regenerate_token
34
- response = Connection.instance.request(:post, "#{member_path}/token")
34
+ response = request(:post, "#{member_path}/token")
35
35
  set_attrs_from_response(response)
36
36
  self
37
37
  end
@@ -41,7 +41,7 @@ module Cloudflare
41
41
  def start_track(meeting_id:, layers:, app_id: nil, account_id: nil, max_seconds: nil)
42
42
  scope = build_scope(account_id: account_id, app_id: app_id)
43
43
  path = interpolate("/accounts/{account_id}/realtime/kit/{app_id}/recordings/track", scope)
44
- response = Connection.instance.request(:post, path,
44
+ response = request(:post, path,
45
45
  body: { meeting_id: meeting_id, layers: layers, max_seconds: max_seconds })
46
46
  new(response, scope: scope)
47
47
  end
@@ -52,7 +52,7 @@ module Cloudflare
52
52
  scope = build_scope(account_id: account_id, app_id: app_id)
53
53
  path = interpolate("/accounts/{account_id}/realtime/kit/{app_id}/recordings/active-recording/{meeting_id}",
54
54
  scope.merge(meeting_id: meeting_id))
55
- response = Connection.instance.request(:get, path)
55
+ response = request(:get, path)
56
56
  new(response, scope: scope)
57
57
  end
58
58
  end
@@ -69,7 +69,7 @@ module Cloudflare
69
69
  unless TRANSITIONS.include?(sym)
70
70
  raise ArgumentError, "action must be one of #{TRANSITIONS.inspect}, got #{action.inspect}"
71
71
  end
72
- response = Connection.instance.request(:put, member_path, body: { action: sym.to_s })
72
+ response = request(:put, member_path, body: { action: sym.to_s })
73
73
  set_attrs_from_response(response)
74
74
  self
75
75
  end
@@ -45,7 +45,7 @@ module Cloudflare
45
45
  scope = build_scope(account_id: account_id, app_id: app_id)
46
46
  path = interpolate("/accounts/{account_id}/realtime/kit/{app_id}/sessions/peer-report/{peer_id}",
47
47
  scope.merge(peer_id: peer_id))
48
- response = Connection.instance.request(:get, path, params: { filters: filters })
48
+ response = request(:get, path, params: { filters: filters })
49
49
  SessionParticipant.new(response, scope: scope)
50
50
  end
51
51
  end
@@ -58,7 +58,7 @@ module Cloudflare
58
58
  # so the two endpoints don't share scope and Relation can't model them
59
59
  # together cleanly.
60
60
  def livestream_sessions(page_no: nil, per_page: nil)
61
- response = Connection.instance.request(:get, "#{member_path}/livestream-sessions",
61
+ response = request(:get, "#{member_path}/livestream-sessions",
62
62
  params: { page_no: page_no, per_page: per_page })
63
63
  Array(self.class.unwrap_envelope(response)).map do |item|
64
64
  LivestreamSession.new(item, scope: { account_id: @scope[:account_id], app_id: @scope[:app_id] })
@@ -19,7 +19,7 @@ module Cloudflare
19
19
 
20
20
  # POST /sessions/{session_id}/summary — generate a summary on demand.
21
21
  def generate
22
- response = Connection.instance.request(:post, member_path)
22
+ response = request(:post, member_path)
23
23
  set_attrs_from_response(response)
24
24
  self
25
25
  end
@@ -27,7 +27,7 @@ module Cloudflare
27
27
 
28
28
  # PUT /webhooks/{id} — replace every field (vs PATCH +update+).
29
29
  def replace(**attrs)
30
- response = Connection.instance.request(:put, member_path, body: self.class.to_wire_keys(attrs))
30
+ response = request(:put, member_path, body: self.class.to_wire_keys(attrs))
31
31
  set_attrs_from_response(response)
32
32
  self
33
33
  end
@@ -3,21 +3,33 @@ module Cloudflare
3
3
  # for meetings, livestreams, recordings, sessions, and analytics. Each
4
4
  # resource maps to a hand-written class under this namespace.
5
5
  #
6
- # == Default app_id
6
+ # == Defaults
7
7
  #
8
8
  # Most resources (Meeting, Participant, Recording, ...) live under an app.
9
9
  # Set +app_id+ once at process boot and every call defaults to it; pass
10
10
  # +app_id:+ per-call only to override.
11
11
  #
12
- # Cloudflare::RealtimeKit.app_id = ENV["REALTIMEKIT_APP_ID"]
12
+ # +api_token+ is the per-product token. Resolution chain when a request
13
+ # fires: per-call +api_token:+ kwarg → +Cloudflare::RealtimeKit.api_token+
14
+ # → +Cloudflare.api_token+ (top-level fallback) → raise.
13
15
  #
14
- # Cloudflare::RealtimeKit::Meeting.create(title: "Standup") # uses default
16
+ # Per-product tokens let you scope each product's credentials tightly
17
+ # (e.g., a token with +Realtime: Edit+ only) and rotate them
18
+ # independently. Top-level +Cloudflare.api_token+ stays as a fallback
19
+ # for callers who don't want per-product separation.
20
+ #
21
+ # Cloudflare::RealtimeKit.api_token = ENV["REALTIMEKIT_API_TOKEN"]
22
+ # Cloudflare::RealtimeKit.app_id = ENV["REALTIMEKIT_APP_ID"]
23
+ #
24
+ # Cloudflare::RealtimeKit::Meeting.create(title: "Standup") # uses defaults
15
25
  # Cloudflare::RealtimeKit::Meeting.find(id, app_id: "other-app-id") # override
16
26
  module RealtimeKit
17
27
  class << self
18
28
  # Default app_id used when a resource call doesn't pass one explicitly.
19
- # Mirrors how +Cloudflare.account_id+ defaults the account scope.
20
29
  attr_accessor :app_id
30
+
31
+ # Per-product API token. Falls back to +Cloudflare.api_token+ when nil.
32
+ attr_accessor :api_token
21
33
  end
22
34
 
23
35
  autoload :App, "cloudflare/realtime_kit/app"
@@ -160,25 +160,42 @@ module Cloudflare
160
160
  def create(**attrs)
161
161
  scope = extract_scope!(attrs)
162
162
  path = interpolate(_collection_path, scope)
163
- response = Connection.instance.request(:post, path, body: to_wire_keys(attrs))
163
+ response = request(:post, path, body: to_wire_keys(attrs))
164
164
  new(response, scope: scope)
165
165
  end
166
166
 
167
167
  def find(id, **scope_attrs)
168
168
  scope = build_scope(scope_attrs)
169
169
  path = interpolate(_member_path, scope.merge(id: id))
170
- response = Connection.instance.request(:get, path)
170
+ response = request(:get, path)
171
171
  new(response, scope: scope)
172
172
  end
173
173
 
174
174
  def all(**params)
175
175
  scope = extract_scope!(params)
176
176
  path = interpolate(_collection_path, scope)
177
- response = Connection.instance.request(:get, path, params: to_wire_keys(params))
177
+ response = request(:get, path, params: to_wire_keys(params))
178
178
  items = unwrap_envelope(response)
179
179
  Array(items).map { new(_1, scope: scope) }
180
180
  end
181
181
 
182
+ # Wraps +Connection.instance.request+ with the api_token resolved from
183
+ # the resource's product namespace. Subclasses (and instance methods,
184
+ # via the same-named instance helper) call this rather than
185
+ # +Connection.instance.request+ directly so the per-product token chain
186
+ # — per-call → product namespace token → top-level — is enforced
187
+ # uniformly.
188
+ def request(method, path, **opts)
189
+ Connection.instance.request(method, path, api_token: configured_api_token, **opts)
190
+ end
191
+
192
+ # Token resolution: per-product accessor (e.g., +Cloudflare::RealtimeKit.api_token+)
193
+ # if set, else +Cloudflare.api_token+. Connection raises when both are nil.
194
+ def configured_api_token
195
+ ns = product_namespace
196
+ (ns && ns.respond_to?(:api_token) && ns.api_token) || Cloudflare.api_token
197
+ end
198
+
182
199
  private
183
200
  # Pulls scope params out of the kwargs hash (mutating). Falls back
184
201
  # to product-level defaults (see +global_default_for+) when a key
@@ -250,18 +267,18 @@ module Cloudflare
250
267
  def attributes = (ensure_loaded!; @attrs)
251
268
 
252
269
  def update(**changes)
253
- response = Connection.instance.request(:patch, member_path, body: self.class.to_wire_keys(changes))
270
+ response = request(:patch, member_path, body: self.class.to_wire_keys(changes))
254
271
  set_attrs_from_response(response)
255
272
  self
256
273
  end
257
274
 
258
275
  def destroy
259
- Connection.instance.request(:delete, member_path)
276
+ request(:delete, member_path)
260
277
  freeze
261
278
  end
262
279
 
263
280
  def reload
264
- response = Connection.instance.request(:get, member_path)
281
+ response = request(:get, member_path)
265
282
  set_attrs_from_response(response)
266
283
  self
267
284
  end
@@ -292,6 +309,14 @@ module Cloudflare
292
309
  reload unless @loaded
293
310
  end
294
311
 
312
+ # Instance-side wrapper around the class-level +request+. Lets custom
313
+ # action methods (e.g., +active_session.kick+) call +request(:post, ...)+
314
+ # without threading the api_token themselves; the class helper resolves
315
+ # it from the product namespace.
316
+ def request(method, path, **opts)
317
+ self.class.send(:request, method, path, **opts)
318
+ end
319
+
295
320
  def member_path
296
321
  self.class.send(:interpolate, self.class._member_path, @scope.merge(id: id))
297
322
  end
@@ -1,3 +1,3 @@
1
1
  module Cloudflare
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudflare-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tokimonki