smplkit 3.0.95 → 3.0.96

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: daa3b1ae6cdf8922badb57509c04e16d007372f82b95661de9f223602f174a2b
4
- data.tar.gz: d03104fec0c8f27c9d5942821b8c49143d6321f4b9790da7be91f7a2aef55ddf
3
+ metadata.gz: a1d765730dd1bb26ebf38f16627878d7d33220bc20bd005b98b12303a18e88c1
4
+ data.tar.gz: 1109c9cd1b02865ae3d2148a8232509b185d828ed8762c025cf82dcfc9c9c09e
5
5
  SHA512:
6
- metadata.gz: 86ef3645c6d7c44b4afd90ac2948762a3ff4d2d2df6076fb524f6f636cac2d0b0335d55e4bd3a6fac974933f65f332a2754eac9ac417d2aff91ab4b7bc57c874
7
- data.tar.gz: 786ad2e7704b39d79090c51b6fc4663e6072fda4e1e3d51fb70abbb272f951a17246351a7a0600c4e56d9c696c82f486f2fda490398a9e0e4aa0e1be4c728c1e
6
+ metadata.gz: '099f6f60a8acdbd5804cd32743d7f6d6e28261de9901f08a354536da0e49a8b3cec0809790106b072cd7275daa60fdd4279b979dca22590ae74d87382bed04ed'
7
+ data.tar.gz: '089e631caa282df42aea0344a2e38fe17795e2d773d6799e6684ee1d389d418b0d4c1714fcf398c70b8961fdd4919b11c1b7b56f26b8e7a6e1b631853ac12447'
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module Smplkit
7
+ module Account
8
+ # Resolve the (app_base_url, api_key, extra_headers) for the settings client.
9
+ #
10
+ # +base_url+/+api_key+ are used directly when both are supplied (the path
11
+ # the top-level client takes after it has already resolved them); otherwise
12
+ # the management config resolver fills in whatever is missing.
13
+ def self.resolve_account_target(api_key:, base_url:, profile:, base_domain:, scheme:, debug:, extra_headers:)
14
+ cfg = ConfigResolution.resolve_management_config(
15
+ profile: profile, api_key: api_key, base_domain: base_domain, scheme: scheme, debug: debug
16
+ )
17
+ resolved_key = api_key.nil? ? cfg.api_key : api_key
18
+ app_url = base_url.nil? ? ConfigResolution.service_url(cfg.scheme, "app", cfg.base_domain) : base_url
19
+ headers = {}
20
+ headers.merge!(cfg.extra_headers || {})
21
+ headers.merge!(extra_headers || {})
22
+ [app_url.sub(%r{/+\z}, ""), resolved_key, headers]
23
+ end
24
+
25
+ # Sync account-settings get/save (+client.account.settings+).
26
+ #
27
+ # The endpoint isn't JSON:API — body is a raw JSON object — so we use an
28
+ # HTTP client directly rather than going through a generated client.
29
+ class SettingsClient
30
+ SETTINGS_PATH = "/api/v1/accounts/current/settings"
31
+
32
+ def initialize(app_base_url, api_key, extra_headers = nil)
33
+ @base_url = app_base_url
34
+ @headers = {
35
+ "Authorization" => "Bearer #{api_key}",
36
+ "Content-Type" => "application/json"
37
+ }.merge(extra_headers || {})
38
+ end
39
+
40
+ def get
41
+ resp = connection.get(SETTINGS_PATH)
42
+ Errors.raise_for_status(resp.status, resp.body.to_s)
43
+ AccountSettings.new(self, data: parse_body(resp.body))
44
+ end
45
+
46
+ def _save(data)
47
+ resp = connection.put(SETTINGS_PATH) { |req| req.body = JSON.generate(data) }
48
+ Errors.raise_for_status(resp.status, resp.body.to_s)
49
+ AccountSettings.new(self, data: parse_body(resp.body))
50
+ end
51
+
52
+ private
53
+
54
+ # A short-lived connection per call — mirrors the Python settings client
55
+ # opening and closing its own HTTP client each time.
56
+ def connection
57
+ Faraday.new(url: @base_url, headers: @headers, request: { timeout: 30 })
58
+ end
59
+
60
+ def parse_body(body)
61
+ return {} if body.nil? || body.to_s.empty?
62
+
63
+ parsed = JSON.parse(body)
64
+ parsed.is_a?(Hash) ? parsed : {}
65
+ rescue JSON::ParserError
66
+ {}
67
+ end
68
+ end
69
+
70
+ # The Smpl Account client (sync).
71
+ #
72
+ # Exposes the authenticated account's own configuration, reachable as
73
+ # +client.account+ (+Smplkit::Client+) or constructed directly:
74
+ #
75
+ # account = Smplkit::AccountClient.new(api_key: "sk_...")
76
+ # settings = account.settings.get
77
+ # settings.environment_order = ["production", "staging"]
78
+ # settings.save
79
+ #
80
+ # Sub-client: +settings+ (get/save). Pure CRUD — no +install+ required.
81
+ #
82
+ # @param api_key [String, nil] API key. When omitted, resolved from
83
+ # +SMPLKIT_API_KEY+ or +~/.smplkit+.
84
+ # @param base_url [String, nil] Full app-service base URL. Usually resolved
85
+ # from +base_domain+/+scheme+; supplied directly by the top-level clients
86
+ # which have already computed it.
87
+ # @param profile [String, nil] Named +~/.smplkit+ profile section.
88
+ # @param base_domain [String, nil] Base domain for API requests (default
89
+ # +"smplkit.com"+).
90
+ # @param scheme [String, nil] URL scheme (default +"https"+).
91
+ # @param debug [Boolean, nil] Enable SDK debug logging.
92
+ # @param extra_headers [Hash, nil] Extra headers attached to every request.
93
+ class AccountClient
94
+ attr_reader :settings
95
+
96
+ def initialize(api_key: nil, base_url: nil, profile: nil, base_domain: nil,
97
+ scheme: nil, debug: nil, extra_headers: nil)
98
+ app_url, resolved_key, headers = Account.resolve_account_target(
99
+ api_key: api_key, base_url: base_url, profile: profile,
100
+ base_domain: base_domain, scheme: scheme, debug: debug, extra_headers: extra_headers
101
+ )
102
+ @settings = SettingsClient.new(app_url, resolved_key, headers.empty? ? nil : headers)
103
+ end
104
+
105
+ # No-op — the settings client opens a short-lived HTTP client per call.
106
+ def close; end
107
+
108
+ # Construct, yield to the block, and close on exit.
109
+ def self.open(**kwargs)
110
+ client = new(**kwargs)
111
+ begin
112
+ yield client
113
+ ensure
114
+ client.close
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ AccountClient = Account::AccountClient
121
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ module Account
5
+ # Active-record account-settings model.
6
+ #
7
+ # The wire format is opaque JSON. Documented keys are exposed as typed
8
+ # properties; unknown keys live in +raw+. +save+ writes the full settings
9
+ # object back.
10
+ class AccountSettings
11
+ def initialize(client = nil, data: nil)
12
+ @client = client
13
+ @data = data ? data.dup : {}
14
+ end
15
+
16
+ # The full settings dict. Mutations are persisted on save.
17
+ def raw
18
+ @data
19
+ end
20
+
21
+ def raw=(value)
22
+ @data = value.dup
23
+ end
24
+
25
+ # Canonical ordering of STANDARD environments. Empty list if unset.
26
+ def environment_order
27
+ Array(@data["environment_order"] || [])
28
+ end
29
+
30
+ def environment_order=(value)
31
+ @data["environment_order"] = value.to_a
32
+ end
33
+
34
+ def save
35
+ raise "AccountSettings was constructed without a client; cannot save" if @client.nil?
36
+
37
+ other = @client._save(@data)
38
+ _apply(other)
39
+ self
40
+ end
41
+ alias save! save
42
+
43
+ def to_s
44
+ "AccountSettings(#{@data.inspect})"
45
+ end
46
+ alias inspect to_s
47
+
48
+ def _apply(other)
49
+ @data = other.raw.dup
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ # Ruby-internal adapters bridging the generated client layer to the wrapper.
5
+ module ApiSupport
6
+ # Default page[size] the runtime asks for when walking a list endpoint to
7
+ # completion. The platform caps page[size] at 1000; using the same value
8
+ # here makes the minimum number of round-trips per exhaustive fetch.
9
+ RUNTIME_PAGE_SIZE = 1000
10
+
11
+ # Wraps a generated-API call and converts any +ApiError+ raised by the
12
+ # generated layer into the +Smplkit::Error+ hierarchy. Connection-level
13
+ # failures (no response from the server) become +Smplkit::ConnectionError+;
14
+ # status-coded failures route through +Errors.raise_for_status+ which emits
15
+ # +NotFoundError+ / +ConflictError+ / +ValidationError+ / +Error+ depending
16
+ # on the JSON:API body.
17
+ module ErrorMapping
18
+ module_function
19
+
20
+ def call
21
+ yield
22
+ rescue StandardError => e
23
+ raise unless generated_api_error?(e)
24
+
25
+ raise Smplkit::ConnectionError, e.message.to_s if e.code.nil? || e.code.zero?
26
+
27
+ Smplkit::Errors.raise_for_status(e.code, e.response_body.to_s)
28
+ # raise_for_status only returns on 2xx; if we get here the generated
29
+ # layer raised on a 2xx (shouldn't happen) so re-raise the original.
30
+ raise
31
+ end
32
+
33
+ def generated_api_error?(err)
34
+ klass_name = err.class.name.to_s
35
+ klass_name.start_with?("SmplkitGeneratedClient::") && klass_name.end_with?("::ApiError")
36
+ end
37
+ end
38
+
39
+ # Walk a generated paginated list endpoint to completion.
40
+ #
41
+ # The block receives a per-page +opts+ hash with +page_number+ and
42
+ # +page_size+ filled in, calls the generated list method through
43
+ # {ErrorMapping.call}, and returns the response object. Pages stop when the
44
+ # server returns fewer rows than requested — the platform's standard
45
+ # last-page signal across every offset-paginated list endpoint. Returns the
46
+ # concatenated +response.data+ rows.
47
+ module PaginatedFetch
48
+ module_function
49
+
50
+ def collect(page_size: RUNTIME_PAGE_SIZE)
51
+ rows = []
52
+ page_number = 1
53
+ loop do
54
+ opts = { page_number: page_number, page_size: page_size }
55
+ response = ErrorMapping.call { yield(opts) }
56
+ page = response.data || []
57
+ rows.concat(page)
58
+ break if page.length < page_size
59
+
60
+ page_number += 1
61
+ end
62
+ rows
63
+ end
64
+ end
65
+
66
+ # Deep-stringify Hash keys so resources returned by generated +to_hash+
67
+ # (symbol-keyed) match what the wrapper helpers expect (string-keyed).
68
+ module ResourceShim
69
+ module_function
70
+
71
+ def stringify(value)
72
+ Smplkit::Helpers.deep_stringify_keys(value)
73
+ end
74
+
75
+ # Convenience: produce a string-keyed Hash from a generated model.
76
+ def from_model(model)
77
+ return {} if model.nil?
78
+
79
+ stringify(model.to_hash)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -2,18 +2,16 @@
2
2
 
3
3
  module Smplkit
4
4
  module Audit
5
- # Audit-product entry point — accessed via +client.audit+.
5
+ # The Smpl Audit client — accessed via +client.audit+.
6
6
  #
7
- # Owns event recording and read-side queries: fire-and-forget
8
- # +#events.record+, plus the audit-log +list+ / +get+ and the
9
- # distinct-value listings (+resource_types+, +event_types+,
10
- # +categories+) that back the Activity tab filter dropdowns.
11
- # ADR-047 §2.7.
12
- #
13
- # SIEM forwarder CRUD lives on {Smplkit::ManagementClient} under
14
- # +mgmt.audit.forwarders.*+.
7
+ # One client exposes the full surface — there is no runtime/management
8
+ # split for audit. Owns event recording and read-side queries:
9
+ # fire-and-forget +#events.record+, plus the audit-log +list+ / +get+ and
10
+ # the distinct-value listings (+resource_types+, +event_types+,
11
+ # +categories+) that back the Activity tab filter dropdowns, plus SIEM
12
+ # forwarder CRUD on +#forwarders+. ADR-047 §2.7.
15
13
  class AuditClient
16
- attr_reader :events, :resource_types, :event_types, :categories
14
+ attr_reader :events, :resource_types, :event_types, :categories, :forwarders
17
15
 
18
16
  SDK_OWNED_HEADERS = %w[authorization content-type user-agent].freeze
19
17
 
@@ -41,6 +39,7 @@ module Smplkit
41
39
  @resource_types = ResourceTypes.new(SmplkitGeneratedClient::Audit::ResourceTypesApi.new(api_client))
42
40
  @event_types = EventTypes.new(SmplkitGeneratedClient::Audit::EventTypesApi.new(api_client))
43
41
  @categories = Categories.new(SmplkitGeneratedClient::Audit::CategoriesApi.new(api_client))
42
+ @forwarders = ForwardersClient.new(SmplkitGeneratedClient::Audit::ForwardersApi.new(api_client))
44
43
  end
45
44
 
46
45
  def _close
@@ -1,40 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # SIEM forwarder CRUD for the Smpl Audit client.
4
+ #
5
+ # Forwarders are part of the single unified audit surface — there is no
6
+ # runtime/management split for audit (see +Smplkit::Audit::AuditClient+). This
7
+ # file holds the forwarder CRUD sub-client that the unified +AuditClient+
8
+ # exposes as +.forwarders+:
9
+ #
10
+ # * +ForwardersClient+ — +forwarders.new/get/list/save/delete+
11
+ #
12
+ # The forwarder model classes (+Forwarder+, +ForwarderEnvironment+, …) live in
13
+ # +lib/smplkit/audit/models.rb+.
3
14
  module Smplkit
4
- module Management
5
- # Audit management surfaceaccessed via +mgmt.audit.forwarders+.
15
+ module Audit
16
+ # Surface for +client.audit.forwarders.*+manage the customer's
17
+ # configured SIEM forwarders.
6
18
  #
7
- # Counterpart to the runtime {Smplkit::Audit::AuditClient}. The
8
- # runtime client owns event recording and read-side queries; this
9
- # surface owns SIEM forwarder CRUD. ADR-047 §2.7.
10
- class AuditNamespace
11
- # @return [ForwardersNamespace] CRUD surface for +mgmt.audit.forwarders+.
12
- attr_reader :forwarders
13
-
14
- def initialize(api_client)
15
- @forwarders = ForwardersNamespace.new(
16
- SmplkitGeneratedClient::Audit::ForwardersApi.new(api_client)
17
- )
18
- end
19
- end
20
-
21
- # +mgmt.audit.forwarders.*+ — manage the customer's configured SIEM
22
- # forwarders.
23
- #
24
- # The active-record entry point is {#new_forwarder}: instantiate a
25
- # draft, mutate fields, then call {Smplkit::Audit::Forwarder#save}.
26
- # The namespace exposes {#list}, {#get}, and {#delete} directly; the
27
- # +_create_forwarder+ / +_update_forwarder+ helpers are private and
28
- # invoked by {Smplkit::Audit::Forwarder#save}.
29
- class ForwardersNamespace
19
+ # The active-record entry point is {#new}: instantiate a draft, mutate
20
+ # fields, then call {Smplkit::Audit::Forwarder#save}. The client exposes
21
+ # {#list}, {#get}, and {#delete} directly; the +_create_forwarder+ /
22
+ # +_update_forwarder+ helpers are invoked by {Smplkit::Audit::Forwarder#save}.
23
+ class ForwardersClient
30
24
  def initialize(api)
31
25
  @api = api
32
26
  end
33
27
 
34
- # Construct an unsaved {Smplkit::Audit::Forwarder} bound to this
35
- # namespace. Call +#save+ on the returned instance to persist.
28
+ # Construct an unsaved {Smplkit::Audit::Forwarder} bound to this client.
29
+ # Call +#save+ on the returned instance to persist.
36
30
  #
37
- # @param name [String] Display name.
31
+ # @param id [String] Caller-supplied unique identifier (the forwarder's
32
+ # key). Unique within the account; immutable. The audit service returns
33
+ # 409 if another live forwarder already uses this id.
34
+ # @param name [String] Display name. Defaults to +id+ when not supplied.
38
35
  # @param forwarder_type [String] One of {Smplkit::Audit::ForwarderType::VALUES}.
39
36
  # @param configuration [Smplkit::Audit::HttpConfiguration] Destination
40
37
  # request configuration. Headers carry credentials and are encrypted at
@@ -56,11 +53,9 @@ module Smplkit
56
53
  # @param filter [Hash, nil] Optional JSON Logic filter; events that don't
57
54
  # match are recorded as +filtered_out+ deliveries.
58
55
  # @param transform [Object, nil] Optional template applied to each event
59
- # before delivery. Free-form by default the audit service passes the
60
- # value verbatim to the engine named by +transform_type+. Must be paired
61
- # with a non-nil +transform_type+; when +transform_type+ is
62
- # +TransformType::JSONATA+, +transform+ must be a +String+ (the JSONata
63
- # expression).
56
+ # before delivery. Must be paired with a non-nil +transform_type+; when
57
+ # +transform_type+ is +TransformType::JSONATA+, +transform+ must be a
58
+ # +String+ (the JSONata expression).
64
59
  # @param transform_type [String, nil] Engine that evaluates +transform+ —
65
60
  # one of {Smplkit::Audit::TransformType::VALUES}. Must be paired with a
66
61
  # non-nil +transform+.
@@ -68,12 +63,12 @@ module Smplkit
68
63
  # nil or both set, or when +transform_type+ is +JSONATA+ and +transform+
69
64
  # is not a +String+.
70
65
  # @return [Smplkit::Audit::Forwarder]
71
- def new_forwarder(id, forwarder_type:, configuration:, name: nil,
72
- environments: nil, description: nil,
73
- forward_smplkit_events: false,
74
- filter: nil, transform: nil, transform_type: nil)
75
- Smplkit::Audit::Forwarder.send(:validate_transform_pair!, transform, transform_type)
76
- Smplkit::Audit::Forwarder.new(
66
+ def new(id, forwarder_type:, configuration:, name: nil,
67
+ environments: nil, description: nil,
68
+ forward_smplkit_events: false,
69
+ filter: nil, transform: nil, transform_type: nil)
70
+ Forwarder.send(:validate_transform_pair!, transform, transform_type)
71
+ Forwarder.new(
77
72
  self,
78
73
  id: id,
79
74
  name: name || id,
@@ -91,33 +86,31 @@ module Smplkit
91
86
  # List forwarders for the authenticated account.
92
87
  #
93
88
  # Offset paginated per ADR-014: pass +page_number+ (1-based) and
94
- # +page_size+ (default 1000, max 1000). Pass +meta_total: true+ to
95
- # populate +total+ and +total_pages+ in the returned +pagination+
96
- # block (costs an extra COUNT query server-side).
89
+ # +page_size+ (default 1000, max 1000). Pass +meta_total: true+ to populate
90
+ # +total+ and +total_pages+ in the returned +pagination+ block (costs an
91
+ # extra COUNT query server-side).
97
92
  #
98
93
  # @return [ForwarderListPage]
99
94
  def list(forwarder_type: nil, page_number: nil, page_size: nil, meta_total: nil)
100
95
  opts = {}
101
- opts[:filter_forwarder_type] = Smplkit::Audit::ForwarderType.coerce(forwarder_type) if forwarder_type
96
+ opts[:filter_forwarder_type] = ForwarderType.coerce(forwarder_type) if forwarder_type
102
97
  opts[:page_number] = page_number if page_number
103
98
  opts[:page_size] = page_size if page_size
104
99
  opts[:meta_total] = meta_total unless meta_total.nil?
105
100
 
106
- resp = Smplkit::Audit.call_api { @api.list_forwarders(opts) }
107
- forwarders = (resp.data || []).map do |r|
108
- Smplkit::Audit::Forwarder.from_resource(r, client: self)
109
- end
110
- ForwarderListPage.new(forwarders, Smplkit::Audit.extract_pagination(resp.meta))
101
+ resp = Audit.call_api { @api.list_forwarders(opts) }
102
+ forwarders = (resp.data || []).map { |r| Forwarder.from_resource(r, client: self) }
103
+ ForwarderListPage.new(forwarders, Audit.extract_pagination(resp.meta))
111
104
  end
112
105
 
113
- # Fetch a single forwarder by id. The returned instance is bound to
114
- # this namespace, so +forwarder.save+ and +forwarder.delete+ work.
106
+ # Fetch a single forwarder by id. The returned instance is bound to this
107
+ # client, so +forwarder.save+ and +forwarder.delete+ work.
115
108
  #
116
109
  # @param forwarder_id [String]
117
110
  # @return [Smplkit::Audit::Forwarder]
118
111
  def get(forwarder_id)
119
- resp = Smplkit::Audit.call_api { @api.get_forwarder(forwarder_id) }
120
- Smplkit::Audit::Forwarder.from_resource(resp.data, client: self)
112
+ resp = Audit.call_api { @api.get_forwarder(forwarder_id) }
113
+ Forwarder.from_resource(resp.data, client: self)
121
114
  end
122
115
 
123
116
  # Soft-delete a forwarder.
@@ -125,7 +118,7 @@ module Smplkit
125
118
  # @param forwarder_id [String]
126
119
  # @return [nil]
127
120
  def delete(forwarder_id)
128
- Smplkit::Audit.call_api { @api.delete_forwarder(forwarder_id) }
121
+ Audit.call_api { @api.delete_forwarder(forwarder_id) }
129
122
  nil
130
123
  end
131
124
 
@@ -136,22 +129,21 @@ module Smplkit
136
129
  raise ArgumentError, "Forwarder.id is required on create (caller-supplied key)"
137
130
  end
138
131
 
139
- resp = Smplkit::Audit.call_api { @api.create_forwarder(build_create_body(forwarder)) }
140
- Smplkit::Audit::Forwarder.from_resource(resp.data, client: self)
132
+ resp = Audit.call_api { @api.create_forwarder(build_create_body(forwarder)) }
133
+ Forwarder.from_resource(resp.data, client: self)
141
134
  end
142
135
 
143
- # @api private — Full-replace PUT for an existing forwarder. Called
144
- # by {Smplkit::Audit::Forwarder#save} on instances with +created_at+.
136
+ # @api private — Full-replace PUT for an existing forwarder. Called by
137
+ # {Smplkit::Audit::Forwarder#save} on instances with +created_at+.
145
138
  #
146
- # Header values must be re-supplied as plaintext; the GET path
147
- # redacts them, so a PUT body containing +"<redacted>"+ would
148
- # persist that literal. Track real header values client-side and
149
- # round-trip them.
139
+ # Header values must be re-supplied as plaintext; the GET path redacts
140
+ # them, so a PUT body containing +"<redacted>"+ would persist that literal.
141
+ # Track real header values client-side and round-trip them.
150
142
  def _update_forwarder(forwarder)
151
143
  raise ArgumentError, "cannot update a Forwarder with no id" if forwarder.id.nil?
152
144
 
153
- resp = Smplkit::Audit.call_api { @api.update_forwarder(forwarder.id, build_body(forwarder)) }
154
- Smplkit::Audit::Forwarder.from_resource(resp.data, client: self)
145
+ resp = Audit.call_api { @api.update_forwarder(forwarder.id, build_body(forwarder)) }
146
+ Forwarder.from_resource(resp.data, client: self)
155
147
  end
156
148
 
157
149
  private
@@ -160,16 +152,15 @@ module Smplkit
160
152
  #
161
153
  # Accepts either {Smplkit::Audit::ForwarderEnvironment} values or plain
162
154
  # hashes (+{ enabled: true, configuration: HttpConfiguration.new(...) }+)
163
- # so callers can use the lightweight hash form without importing the
164
- # model.
155
+ # so callers can use the lightweight hash form without importing the model.
165
156
  def normalize_environments(environments)
166
157
  return {} if environments.nil? || environments.empty?
167
158
 
168
159
  environments.each_with_object({}) do |(env_key, value), out|
169
- out[env_key.to_s] = if value.is_a?(Smplkit::Audit::ForwarderEnvironment)
160
+ out[env_key.to_s] = if value.is_a?(ForwarderEnvironment)
170
161
  value
171
162
  else
172
- Smplkit::Audit::ForwarderEnvironment.new(
163
+ ForwarderEnvironment.new(
173
164
  enabled: value[:enabled] || value["enabled"] || false,
174
165
  configuration: value[:configuration] || value["configuration"]
175
166
  )
@@ -186,7 +177,7 @@ module Smplkit
186
177
  (environments || {}).each_with_object({}) do |(env_key, env), out|
187
178
  out[env_key.to_s] = SmplkitGeneratedClient::Audit::ForwarderEnvironment.new(
188
179
  enabled: env.enabled,
189
- configuration: env.configuration.nil? ? nil : Smplkit::Audit::HttpConfiguration.to_wire(env.configuration)
180
+ configuration: env.configuration.nil? ? nil : HttpConfiguration.to_wire(env.configuration)
190
181
  )
191
182
  end
192
183
  end
@@ -197,20 +188,20 @@ module Smplkit
197
188
  SmplkitGeneratedClient::Audit::Forwarder.new(
198
189
  name: forwarder.name,
199
190
  description: forwarder.description,
200
- forwarder_type: Smplkit::Audit::ForwarderType.coerce(forwarder.forwarder_type),
191
+ forwarder_type: ForwarderType.coerce(forwarder.forwarder_type),
201
192
  forward_smplkit_events: forwarder.forward_smplkit_events,
202
193
  environments: environments_to_wire(forwarder.environments),
203
194
  filter: forwarder.filter,
204
- transform_type: Smplkit::Audit::TransformType.coerce(forwarder.transform_type),
195
+ transform_type: TransformType.coerce(forwarder.transform_type),
205
196
  transform: forwarder.transform,
206
- configuration: Smplkit::Audit::HttpConfiguration.to_wire(forwarder.configuration)
197
+ configuration: HttpConfiguration.to_wire(forwarder.configuration)
207
198
  )
208
199
  end
209
200
 
210
201
  def build_create_body(forwarder)
211
- # Create uses the distinct ForwarderCreateRequest envelope; the
212
- # audit service requires data.id (the customer-supplied key) on
213
- # create and 409s on conflict.
202
+ # Create uses the distinct ForwarderCreateRequest envelope; the audit
203
+ # service requires data.id (the customer-supplied key) on create and
204
+ # 409s on conflict.
214
205
  resource = SmplkitGeneratedClient::Audit::ForwarderCreateResource.new(
215
206
  id: forwarder.id.to_s,
216
207
  type: "forwarder",
@@ -230,13 +221,19 @@ module Smplkit
230
221
  end
231
222
  end
232
223
 
233
- # A single page returned from {ForwardersNamespace#list}.
224
+ # A single page returned from {ForwardersClient#list}.
234
225
  #
235
226
  # @!attribute [rw] forwarders
236
227
  # @return [Array<Smplkit::Audit::Forwarder>] Forwarders in this page.
237
228
  # @!attribute [rw] pagination
238
229
  # @return [Hash] +meta.pagination+ block (+:page+, +:size+, and — only when
239
230
  # the caller passed +meta_total: true+ — +:total+ / +:total_pages+).
240
- ForwarderListPage = Struct.new(:forwarders, :pagination)
231
+ ForwarderListPage = Struct.new(:forwarders, :pagination) do
232
+ include Enumerable
233
+
234
+ def each(&) = forwarders.each(&)
235
+ def length = forwarders.length
236
+ alias_method :size, :length
237
+ end
241
238
  end
242
239
  end
@@ -422,7 +422,7 @@ module Smplkit
422
422
  # A SIEM streaming forwarder configured on the customer's account.
423
423
  #
424
424
  # Active-record style: instantiate via
425
- # +mgmt.audit.forwarders.new_forwarder(...)+, mutate fields directly,
425
+ # +client.audit.forwarders.new(...)+, mutate fields directly,
426
426
  # and call {#save} to persist or {#delete} to remove. Header values in
427
427
  # +configuration.headers+ are returned redacted on reads — the GET path
428
428
  # on the audit API replaces every header value with +"<redacted>"+.
@@ -558,6 +558,45 @@ module Smplkit
558
558
  end
559
559
  alias delete! delete
560
560
 
561
+ # Set this forwarder's destination configuration in memory.
562
+ #
563
+ # With +environment+ omitted, replaces the base {#configuration}. With
564
+ # +environment+ given, sets the per-environment override's configuration
565
+ # on {#environments}, creating the override entry if it doesn't exist yet
566
+ # (preserving any already-set +enabled+ on it). Call {#save} to persist.
567
+ def set_configuration(configuration, environment: nil)
568
+ if environment.nil?
569
+ @configuration = configuration
570
+ else
571
+ _environment_override(environment).configuration = configuration
572
+ end
573
+ end
574
+
575
+ # Set this forwarder's enablement in memory.
576
+ #
577
+ # With +environment+ omitted, sets the base {#enabled} (which the server
578
+ # pins false regardless — enablement is per-environment). With
579
+ # +environment+ given, sets the per-environment override's +enabled+ on
580
+ # {#environments}, creating the override entry if it doesn't exist yet
581
+ # (preserving any already-set +configuration+ on it). Call {#save} to
582
+ # persist.
583
+ def set_enabled(enabled, environment: nil)
584
+ if environment.nil?
585
+ @enabled = enabled
586
+ else
587
+ _environment_override(environment).enabled = enabled
588
+ end
589
+ end
590
+
591
+ # Return the override for +environment+, creating an empty one if absent.
592
+ #
593
+ # The per-environment mutators reach through here so an existing
594
+ # override's other field is preserved when only one of +enabled+ /
595
+ # +configuration+ is being set.
596
+ def _environment_override(environment)
597
+ @environments[environment] ||= ForwarderEnvironment.new
598
+ end
599
+
561
600
  # @api private
562
601
  def _apply(other)
563
602
  @id = other.id