cloudflare-ruby 0.1.2 → 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: 2e55c381f1697329fa6cb8bd393493b4b63b31d0f3b4b672d43e61c2799c0c40
4
- data.tar.gz: dba84759f533e9203bb0f70404b4d719ece8509a883bb83d2aa5accd607d0375
3
+ metadata.gz: 230de80c474a3f8441f60687160aba0f2cd53c5db2d9465498866994150cc60f
4
+ data.tar.gz: 470768dc30305293361258880a74be07ea42832449ec4a77cae0079d42ca3cbc
5
5
  SHA512:
6
- metadata.gz: e90e2f05d9b52b24195ff9d95c9849ff04290ac8d460ec0e59917869125e03cdc176a4717b2c1895891b463076d04ddbf30b17f4a8c52693d0f365bd6bef025f
7
- data.tar.gz: 364098f7920cebdb13e6751be1bd28c32ba74b144e5a6f06e947a333a1b49b27efd5da9ae6befa3361253171df1755aa51816494f2b2821ee0190830733337e5
6
+ metadata.gz: 3416c0f5b271e376182525b183324d5ec7e0c11e5437f13194a75f35265c8463ac6d843414386e4fb1954966c340515f6eee2540e10b67bec1d6b50eae98574b
7
+ data.tar.gz: 7c2d7a9a594d06f674af34d26cf9a9570da463ac81e817aa56738d2a0332760c0116550c3adf68b1616cd019f33b2e9ccd24dbcf8ab7e414ef3e222d57e5e68d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
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
+
20
+ ## 0.2.0
21
+
22
+ - **Default `app_id` per process.** Set `Cloudflare::RealtimeKit.app_id`
23
+ once at boot and every RealtimeKit call defaults to it; pass `app_id:`
24
+ per-call only to override. Mirrors the existing `Cloudflare.account_id`
25
+ pattern. Eliminates `app_id:` repetition in apps that talk to a single
26
+ RealtimeKit app per process (the common case).
27
+ - `Resource.build_scope` and `extract_scope!` now consult a generic
28
+ `global_default_for(key)` helper that resolves `:account_id` against
29
+ `Cloudflare.account_id` and any other key against a same-named
30
+ accessor on the resource's product namespace
31
+ (e.g., `:app_id` → `Cloudflare::RealtimeKit.app_id`). Generalizes for
32
+ future product surfaces.
33
+ - `Recording.start_track`, `Recording.active_for`,
34
+ `Session.find_participant_by_peer_id`, `Analytics.daywise`, and
35
+ `Analytics.livestreams_overall` now accept `app_id:` as optional with
36
+ the default falling through.
37
+
3
38
  ## 0.1.2
4
39
 
5
40
  - Add `require "bundler/gem_tasks"` to the Rakefile so `rake release`
@@ -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,14 +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
 
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
+
15
20
  RK = Cloudflare::RealtimeKit # alias to taste
16
21
  ```
17
22
 
18
- After that, `account_id:` is implicit on every call — pass it 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.
19
26
 
20
27
  ## Apps
21
28
 
@@ -28,6 +35,8 @@ RK::App.all # GET /apps
28
35
 
29
36
  ## Meetings
30
37
 
38
+ Most calls below show `app_id:` for clarity. With `Cloudflare::RealtimeKit.app_id` set globally (see Setup), you can omit it.
39
+
31
40
  ```ruby
32
41
  meeting = RK::Meeting.create(app_id: app.id, title: "Standup", record_on_start: true)
33
42
  meeting.id # => "mtg-…"
@@ -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
@@ -18,14 +18,14 @@ module Cloudflare
18
18
  module Analytics
19
19
  class << self
20
20
  # GET /analytics/daywise — daily breakdown of meeting activity.
21
- def daywise(app_id:, start_date:, end_date:, account_id: nil)
21
+ def daywise(start_date:, end_date:, app_id: nil, account_id: nil)
22
22
  fetch("/accounts/{account_id}/realtime/kit/{app_id}/analytics/daywise",
23
23
  app_id: app_id, account_id: account_id,
24
24
  params: { start_date: start_date, end_date: end_date })
25
25
  end
26
26
 
27
27
  # GET /analytics/livestreams/overall — overall livestream metrics.
28
- def livestreams_overall(app_id:, start_time:, end_time:, account_id: nil)
28
+ def livestreams_overall(start_time:, end_time:, app_id: nil, account_id: nil)
29
29
  fetch("/accounts/{account_id}/realtime/kit/{app_id}/analytics/livestreams/overall",
30
30
  app_id: app_id, account_id: account_id,
31
31
  params: { start_time: start_time, end_time: end_time })
@@ -35,17 +35,23 @@ 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
 
42
43
  # Local copies of the two private helpers from Resource — Analytics
43
44
  # isn't a Resource subclass (no instances, just a function namespace),
44
- # so it can't inherit the private class methods.
45
+ # so it can't inherit the private class methods. Falls back to
46
+ # +Cloudflare.account_id+ and +Cloudflare::RealtimeKit.app_id+ for
47
+ # the same per-process default behavior the Resource subclasses
48
+ # get for free.
45
49
  def build_scope(account_id:, app_id:)
46
50
  account = account_id || Cloudflare.account_id
51
+ app = app_id || RealtimeKit.app_id
47
52
  raise ArgumentError, "missing required scope param: account_id" unless account
48
- { account_id: account, app_id: app_id }
53
+ raise ArgumentError, "missing required scope param: app_id" unless app
54
+ { account_id: account, app_id: app }
49
55
  end
50
56
 
51
57
  def interpolate(path, values)
@@ -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
@@ -38,21 +38,21 @@ module Cloudflare
38
38
  # POST /recordings/track — start a multi-file (per-track) recording.
39
39
  # Different upstream endpoint than +create+; use this when you need
40
40
  # one media file per participant track.
41
- def start_track(meeting_id:, layers:, app_id:, account_id: nil, max_seconds: nil)
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
48
48
 
49
49
  # GET /recordings/active-recording/{meeting_id} — fetch the recording
50
50
  # currently in progress for a meeting (404 if none).
51
- def active_for(meeting_id:, app_id:, account_id: nil)
51
+ def active_for(meeting_id:, app_id: nil, account_id: nil)
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
@@ -41,11 +41,11 @@ module Cloudflare
41
41
  # GET /sessions/peer-report/{peer_id} — reverse lookup from a peer id.
42
42
  # Returns participant-shaped data, not a session, so we expose it as a
43
43
  # SessionParticipant instance.
44
- def find_participant_by_peer_id(peer_id, app_id:, account_id: nil, filters: nil)
44
+ def find_participant_by_peer_id(peer_id, app_id: nil, account_id: nil, filters: nil)
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
@@ -2,7 +2,36 @@ module Cloudflare
2
2
  # Cloudflare RealtimeKit (formerly Dyte) — managed video/audio infrastructure
3
3
  # for meetings, livestreams, recordings, sessions, and analytics. Each
4
4
  # resource maps to a hand-written class under this namespace.
5
+ #
6
+ # == Defaults
7
+ #
8
+ # Most resources (Meeting, Participant, Recording, ...) live under an app.
9
+ # Set +app_id+ once at process boot and every call defaults to it; pass
10
+ # +app_id:+ per-call only to override.
11
+ #
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.
15
+ #
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
25
+ # Cloudflare::RealtimeKit::Meeting.find(id, app_id: "other-app-id") # override
5
26
  module RealtimeKit
27
+ class << self
28
+ # Default app_id used when a resource call doesn't pass one explicitly.
29
+ attr_accessor :app_id
30
+
31
+ # Per-product API token. Falls back to +Cloudflare.api_token+ when nil.
32
+ attr_accessor :api_token
33
+ end
34
+
6
35
  autoload :App, "cloudflare/realtime_kit/app"
7
36
  autoload :Meeting, "cloudflare/realtime_kit/meeting"
8
37
  autoload :Participant, "cloudflare/realtime_kit/participant"
@@ -160,31 +160,49 @@ 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
- # Pulls scope params out of the kwargs hash (mutating). Defaults
184
- # account_id from global config if not provided.
200
+ # Pulls scope params out of the kwargs hash (mutating). Falls back
201
+ # to product-level defaults (see +global_default_for+) when a key
202
+ # isn't provided.
185
203
  def extract_scope!(attrs)
186
204
  scope_params.each_with_object({}) do |key, h|
187
- value = attrs.delete(key) || (key == :account_id && Cloudflare.account_id)
205
+ value = attrs.delete(key) || global_default_for(key)
188
206
  raise ArgumentError, "missing required scope param: #{key}" unless value
189
207
  h[key] = value
190
208
  end
@@ -192,12 +210,35 @@ module Cloudflare
192
210
 
193
211
  def build_scope(provided)
194
212
  scope_params.each_with_object({}) do |key, h|
195
- value = provided[key] || (key == :account_id && Cloudflare.account_id)
213
+ value = provided[key] || global_default_for(key)
196
214
  raise ArgumentError, "missing required scope param: #{key}" unless value
197
215
  h[key] = value
198
216
  end
199
217
  end
200
218
 
219
+ # Resolve a scope key to its module-level default:
220
+ # +:account_id+ → +Cloudflare.account_id+ (top-level config), and any
221
+ # other key → a same-named accessor on the resource's product
222
+ # namespace (e.g., +:app_id+ → +Cloudflare::RealtimeKit.app_id+).
223
+ # Lets callers configure once per process and stop repeating
224
+ # +app_id:+ at every call site. Returns nil when no default is set,
225
+ # which lets the caller raise the +missing required scope+ error.
226
+ def global_default_for(key)
227
+ return Cloudflare.account_id if key == :account_id
228
+ namespace = product_namespace
229
+ namespace&.respond_to?(key) ? namespace.public_send(key) : nil
230
+ end
231
+
232
+ # +Cloudflare::RealtimeKit::Meeting+ → +Cloudflare::RealtimeKit+.
233
+ # Returns nil for resources without a product namespace (i.e.,
234
+ # +Cloudflare::Resource+ direct subclasses, which we don't have in
235
+ # practice).
236
+ def product_namespace
237
+ parts = name.split("::")
238
+ return nil if parts.size < 3
239
+ parts[0..-2].inject(Object) { |const, seg| const.const_get(seg) }
240
+ end
241
+
201
242
  def interpolate(path, values)
202
243
  path.gsub(/\{(\w+)\}/) do
203
244
  key = $1.to_sym
@@ -226,18 +267,18 @@ module Cloudflare
226
267
  def attributes = (ensure_loaded!; @attrs)
227
268
 
228
269
  def update(**changes)
229
- 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))
230
271
  set_attrs_from_response(response)
231
272
  self
232
273
  end
233
274
 
234
275
  def destroy
235
- Connection.instance.request(:delete, member_path)
276
+ request(:delete, member_path)
236
277
  freeze
237
278
  end
238
279
 
239
280
  def reload
240
- response = Connection.instance.request(:get, member_path)
281
+ response = request(:get, member_path)
241
282
  set_attrs_from_response(response)
242
283
  self
243
284
  end
@@ -268,6 +309,14 @@ module Cloudflare
268
309
  reload unless @loaded
269
310
  end
270
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
+
271
320
  def member_path
272
321
  self.class.send(:interpolate, self.class._member_path, @scope.merge(id: id))
273
322
  end
@@ -1,3 +1,3 @@
1
1
  module Cloudflare
2
- VERSION = "0.1.2"
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.1.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tokimonki