smplkit 3.0.95 → 3.0.97

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/smplkit/account/client.rb +128 -0
  3. data/lib/smplkit/account/models.rb +71 -0
  4. data/lib/smplkit/api_support.rb +91 -0
  5. data/lib/smplkit/audit/buffer.rb +3 -1
  6. data/lib/smplkit/audit/categories.rb +21 -10
  7. data/lib/smplkit/audit/client.rb +18 -9
  8. data/lib/smplkit/audit/event_types.rb +26 -10
  9. data/lib/smplkit/audit/events.rb +93 -17
  10. data/lib/smplkit/{management/audit.rb → audit/forwarders.rb} +93 -85
  11. data/lib/smplkit/audit/models.rb +86 -32
  12. data/lib/smplkit/audit/resource_types.rb +21 -9
  13. data/lib/smplkit/buffers.rb +250 -0
  14. data/lib/smplkit/client.rb +161 -70
  15. data/lib/smplkit/config/client.rb +874 -186
  16. data/lib/smplkit/config/helpers.rb +44 -6
  17. data/lib/smplkit/config/models.rb +114 -7
  18. data/lib/smplkit/config_resolution.rb +17 -9
  19. data/lib/smplkit/errors.rb +14 -3
  20. data/lib/smplkit/flags/client.rb +602 -116
  21. data/lib/smplkit/flags/models.rb +110 -8
  22. data/lib/smplkit/flags/types.rb +8 -9
  23. data/lib/smplkit/jobs/client.rb +306 -0
  24. data/lib/smplkit/jobs/models.rb +47 -18
  25. data/lib/smplkit/logging/client.rb +755 -191
  26. data/lib/smplkit/logging/helpers.rb +5 -1
  27. data/lib/smplkit/logging/levels.rb +3 -1
  28. data/lib/smplkit/logging/models.rb +163 -6
  29. data/lib/smplkit/logging/normalize.rb +3 -1
  30. data/lib/smplkit/logging/resolution.rb +4 -4
  31. data/lib/smplkit/logging/sources.rb +1 -1
  32. data/lib/smplkit/platform/client.rb +597 -0
  33. data/lib/smplkit/platform/models.rb +282 -0
  34. data/lib/smplkit/{management → platform}/types.rb +21 -4
  35. data/lib/smplkit/transport.rb +103 -0
  36. data/lib/smplkit/ws.rb +1 -1
  37. data/lib/smplkit.rb +18 -6
  38. metadata +11 -7
  39. data/lib/smplkit/management/buffer.rb +0 -198
  40. data/lib/smplkit/management/client.rb +0 -1074
  41. data/lib/smplkit/management/jobs.rb +0 -226
  42. data/lib/smplkit/management/models.rb +0 -178
@@ -1,44 +1,42 @@
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
- # request configuration. Headers carry credentials and are encrypted at
41
- # rest server-side; reads return them redacted.
37
+ # request configuration. Header values often carry credentials and are
38
+ # returned in plaintext on reads, so a get-mutate-put round-trip
39
+ # preserves them without re-entering secrets.
42
40
  # @param environments [Hash{String => Smplkit::Audit::ForwarderEnvironment, Hash}, nil]
43
41
  # Per-environment overrides keyed by environment key (e.g.
44
42
  # +"production"+). A forwarder delivers in an environment only when that
@@ -56,11 +54,9 @@ module Smplkit
56
54
  # @param filter [Hash, nil] Optional JSON Logic filter; events that don't
57
55
  # match are recorded as +filtered_out+ deliveries.
58
56
  # @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).
57
+ # before delivery. Must be paired with a non-nil +transform_type+; when
58
+ # +transform_type+ is +TransformType::JSONATA+, +transform+ must be a
59
+ # +String+ (the JSONata expression).
64
60
  # @param transform_type [String, nil] Engine that evaluates +transform+ —
65
61
  # one of {Smplkit::Audit::TransformType::VALUES}. Must be paired with a
66
62
  # non-nil +transform+.
@@ -68,12 +64,12 @@ module Smplkit
68
64
  # nil or both set, or when +transform_type+ is +JSONATA+ and +transform+
69
65
  # is not a +String+.
70
66
  # @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(
67
+ def new(id, forwarder_type:, configuration:, name: nil,
68
+ environments: nil, description: nil,
69
+ forward_smplkit_events: false,
70
+ filter: nil, transform: nil, transform_type: nil)
71
+ Forwarder.send(:validate_transform_pair!, transform, transform_type)
72
+ Forwarder.new(
77
73
  self,
78
74
  id: id,
79
75
  name: name || id,
@@ -90,42 +86,50 @@ module Smplkit
90
86
 
91
87
  # List forwarders for the authenticated account.
92
88
  #
93
- # 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
+ # Offset paginated: pass +page_number+ (1-based) and +page_size+
90
+ # (default 1000, max 1000).
97
91
  #
98
- # @return [ForwarderListPage]
92
+ # @param forwarder_type [String, nil] Restrict the listing to forwarders of
93
+ # this {Smplkit::Audit::ForwarderType}. Omit to list every type.
94
+ # @param page_number [Integer, nil] 1-based page index. Omit for the first
95
+ # page.
96
+ # @param page_size [Integer, nil] Maximum number of forwarders to return in
97
+ # this page (default 1000, max 1000).
98
+ # @param meta_total [Boolean, nil] When +true+, populate +total+ and
99
+ # +total_pages+ in the returned page's +pagination+ block (costs an extra
100
+ # count server-side). Omit to skip it.
101
+ # @return [ForwarderListPage] A page of the matching forwarders.
99
102
  def list(forwarder_type: nil, page_number: nil, page_size: nil, meta_total: nil)
100
103
  opts = {}
101
- opts[:filter_forwarder_type] = Smplkit::Audit::ForwarderType.coerce(forwarder_type) if forwarder_type
104
+ opts[:filter_forwarder_type] = ForwarderType.coerce(forwarder_type) if forwarder_type
102
105
  opts[:page_number] = page_number if page_number
103
106
  opts[:page_size] = page_size if page_size
104
107
  opts[:meta_total] = meta_total unless meta_total.nil?
105
108
 
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))
109
+ resp = Audit.call_api { @api.list_forwarders(opts) }
110
+ forwarders = (resp.data || []).map { |r| Forwarder.from_resource(r, client: self) }
111
+ ForwarderListPage.new(forwarders, Audit.extract_pagination(resp.meta))
111
112
  end
112
113
 
113
- # Fetch a single forwarder by id. The returned instance is bound to
114
- # this namespace, so +forwarder.save+ and +forwarder.delete+ work.
114
+ # Fetch a single forwarder by id. The returned instance is bound to this
115
+ # client, so +forwarder.save+ and +forwarder.delete+ work. Header values
116
+ # come back in plaintext, so mutating the returned forwarder and calling
117
+ # +save+ preserves them without re-entering secrets.
115
118
  #
116
- # @param forwarder_id [String]
117
- # @return [Smplkit::Audit::Forwarder]
119
+ # @param forwarder_id [String] The forwarder's id (key).
120
+ # @return [Smplkit::Audit::Forwarder] The matching forwarder, bound to this client.
121
+ # @raise [Smplkit::NotFoundError] If no live forwarder with that id exists.
118
122
  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)
123
+ resp = Audit.call_api { @api.get_forwarder(forwarder_id) }
124
+ Forwarder.from_resource(resp.data, client: self)
121
125
  end
122
126
 
123
- # Soft-delete a forwarder.
127
+ # Delete a forwarder.
124
128
  #
125
129
  # @param forwarder_id [String]
126
130
  # @return [nil]
127
131
  def delete(forwarder_id)
128
- Smplkit::Audit.call_api { @api.delete_forwarder(forwarder_id) }
132
+ Audit.call_api { @api.delete_forwarder(forwarder_id) }
129
133
  nil
130
134
  end
131
135
 
@@ -136,22 +140,21 @@ module Smplkit
136
140
  raise ArgumentError, "Forwarder.id is required on create (caller-supplied key)"
137
141
  end
138
142
 
139
- resp = Smplkit::Audit.call_api { @api.create_forwarder(build_create_body(forwarder)) }
140
- Smplkit::Audit::Forwarder.from_resource(resp.data, client: self)
143
+ resp = Audit.call_api { @api.create_forwarder(build_create_body(forwarder)) }
144
+ Forwarder.from_resource(resp.data, client: self)
141
145
  end
142
146
 
143
- # @api private — Full-replace PUT for an existing forwarder. Called
144
- # by {Smplkit::Audit::Forwarder#save} on instances with +created_at+.
147
+ # @api private — Full-replace PUT for an existing forwarder. Called by
148
+ # {Smplkit::Audit::Forwarder#save} on instances with +created_at+.
145
149
  #
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.
150
+ # Header values come back in plaintext on the GET path, so a fetched
151
+ # forwarder round-trips through this full-replace PUT with its header
152
+ # values intact no need to re-enter secrets.
150
153
  def _update_forwarder(forwarder)
151
154
  raise ArgumentError, "cannot update a Forwarder with no id" if forwarder.id.nil?
152
155
 
153
- resp = Smplkit::Audit.call_api { @api.update_forwarder(forwarder.id, build_body(forwarder)) }
154
- Smplkit::Audit::Forwarder.from_resource(resp.data, client: self)
156
+ resp = Audit.call_api { @api.update_forwarder(forwarder.id, build_body(forwarder)) }
157
+ Forwarder.from_resource(resp.data, client: self)
155
158
  end
156
159
 
157
160
  private
@@ -160,16 +163,15 @@ module Smplkit
160
163
  #
161
164
  # Accepts either {Smplkit::Audit::ForwarderEnvironment} values or plain
162
165
  # hashes (+{ enabled: true, configuration: HttpConfiguration.new(...) }+)
163
- # so callers can use the lightweight hash form without importing the
164
- # model.
166
+ # so callers can use the lightweight hash form without importing the model.
165
167
  def normalize_environments(environments)
166
168
  return {} if environments.nil? || environments.empty?
167
169
 
168
170
  environments.each_with_object({}) do |(env_key, value), out|
169
- out[env_key.to_s] = if value.is_a?(Smplkit::Audit::ForwarderEnvironment)
171
+ out[env_key.to_s] = if value.is_a?(ForwarderEnvironment)
170
172
  value
171
173
  else
172
- Smplkit::Audit::ForwarderEnvironment.new(
174
+ ForwarderEnvironment.new(
173
175
  enabled: value[:enabled] || value["enabled"] || false,
174
176
  configuration: value[:configuration] || value["configuration"]
175
177
  )
@@ -186,31 +188,31 @@ module Smplkit
186
188
  (environments || {}).each_with_object({}) do |(env_key, env), out|
187
189
  out[env_key.to_s] = SmplkitGeneratedClient::Audit::ForwarderEnvironment.new(
188
190
  enabled: env.enabled,
189
- configuration: env.configuration.nil? ? nil : Smplkit::Audit::HttpConfiguration.to_wire(env.configuration)
191
+ configuration: env.configuration.nil? ? nil : HttpConfiguration.to_wire(env.configuration)
190
192
  )
191
193
  end
192
194
  end
193
195
 
194
196
  def build_attrs(forwarder)
195
- # The base ``enabled`` is server-pinned false (ADR-055); we don't send
196
- # it. Enablement travels entirely through ``environments``.
197
+ # The base +enabled+ is server-pinned false; we don't send it.
198
+ # Enablement travels entirely through +environments+.
197
199
  SmplkitGeneratedClient::Audit::Forwarder.new(
198
200
  name: forwarder.name,
199
201
  description: forwarder.description,
200
- forwarder_type: Smplkit::Audit::ForwarderType.coerce(forwarder.forwarder_type),
202
+ forwarder_type: ForwarderType.coerce(forwarder.forwarder_type),
201
203
  forward_smplkit_events: forwarder.forward_smplkit_events,
202
204
  environments: environments_to_wire(forwarder.environments),
203
205
  filter: forwarder.filter,
204
- transform_type: Smplkit::Audit::TransformType.coerce(forwarder.transform_type),
206
+ transform_type: TransformType.coerce(forwarder.transform_type),
205
207
  transform: forwarder.transform,
206
- configuration: Smplkit::Audit::HttpConfiguration.to_wire(forwarder.configuration)
208
+ configuration: HttpConfiguration.to_wire(forwarder.configuration)
207
209
  )
208
210
  end
209
211
 
210
212
  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.
213
+ # Create uses the distinct ForwarderCreateRequest envelope; the audit
214
+ # service requires data.id (the customer-supplied key) on create and
215
+ # 409s on conflict.
214
216
  resource = SmplkitGeneratedClient::Audit::ForwarderCreateResource.new(
215
217
  id: forwarder.id.to_s,
216
218
  type: "forwarder",
@@ -230,13 +232,19 @@ module Smplkit
230
232
  end
231
233
  end
232
234
 
233
- # A single page returned from {ForwardersNamespace#list}.
235
+ # A single page returned from {ForwardersClient#list}.
234
236
  #
235
237
  # @!attribute [rw] forwarders
236
238
  # @return [Array<Smplkit::Audit::Forwarder>] Forwarders in this page.
237
239
  # @!attribute [rw] pagination
238
240
  # @return [Hash] +meta.pagination+ block (+:page+, +:size+, and — only when
239
241
  # the caller passed +meta_total: true+ — +:total+ / +:total_pages+).
240
- ForwarderListPage = Struct.new(:forwarders, :pagination)
242
+ ForwarderListPage = Struct.new(:forwarders, :pagination) do
243
+ include Enumerable
244
+
245
+ def each(&) = forwarders.each(&)
246
+ def length = forwarders.length
247
+ alias_method :size, :length
248
+ end
241
249
  end
242
250
  end
@@ -8,6 +8,8 @@ module Smplkit
8
8
  # failures route through {Smplkit::Errors.raise_for_status}, which
9
9
  # emits +PaymentRequiredError+ / +NotFoundError+ / +ConflictError+
10
10
  # / +ValidationError+ / +Error+ depending on the JSON:API body.
11
+ #
12
+ # @api private
11
13
  def self.call_api
12
14
  yield
13
15
  rescue SmplkitGeneratedClient::Audit::ApiError => e
@@ -24,6 +26,8 @@ module Smplkit
24
26
  # URL. Returns nil for non-string input or when the link carries
25
27
  # no cursor parameter; trims trailing query params at the next
26
28
  # ampersand so they don't leak into the token.
29
+ #
30
+ # @api private
27
31
  def self.next_cursor(link)
28
32
  return nil unless link.is_a?(String)
29
33
 
@@ -39,6 +43,8 @@ module Smplkit
39
43
  # Returns a hash with +:page+/+:size+ (and +:total+/+:total_pages+ when
40
44
  # the request opted into +meta[total]=true+). Always returns a hash so
41
45
  # callers don't have to nil-check before reading individual keys.
46
+ #
47
+ # @api private
42
48
  def self.extract_pagination(meta)
43
49
  pagination = meta&.pagination
44
50
  return {} if pagination.nil?
@@ -56,14 +62,16 @@ module Smplkit
56
62
  # The audit read endpoints (events list, the resource_type / event_type /
57
63
  # category discovery lists) accept an optional comma-separated
58
64
  # +filter[environment]+ of real environment keys and/or the reserved
59
- # +"smplkit"+ control-plane bucket (ADR-055). The wrapper takes an
60
- # array of keys for an ergonomic surface and joins it here.
65
+ # +"smplkit"+ control-plane bucket. The wrapper takes an array of keys for
66
+ # an ergonomic surface and joins it here.
61
67
  #
62
68
  # +nil+ or an empty array (or one whose entries are all blank) returns
63
69
  # +nil+ so the caller omits the query param entirely and behaves exactly
64
70
  # as before — existing callers are byte-for-byte unchanged on the wire.
65
71
  # +"smplkit"+ is passed through like any other key; it carries no special
66
72
  # handling in the SDK.
73
+ #
74
+ # @api private
67
75
  def self.join_environments(environments)
68
76
  return nil if environments.nil?
69
77
 
@@ -71,12 +79,12 @@ module Smplkit
71
79
  values.empty? ? nil : values.join(",")
72
80
  end
73
81
 
74
- # Supported SIEM forwarder destination types (ADR-047 §2.12).
82
+ # Supported SIEM forwarder destination types.
75
83
  #
76
84
  # Members are declared in alphabetical order. Customers pass these
77
- # constants — or the equivalent string — to the management
78
- # +forwarders+ surface; the wrapper validates membership via {coerce}
79
- # before round-tripping to the wire.
85
+ # constants — or the equivalent string — to the forwarders surface
86
+ # (+client.audit.forwarders+); the wrapper validates membership via
87
+ # {coerce} before round-tripping to the wire.
80
88
  module ForwarderType
81
89
  DATADOG = "datadog"
82
90
  ELASTIC = "elastic"
@@ -104,7 +112,7 @@ module Smplkit
104
112
  end
105
113
  end
106
114
 
107
- # HTTP verb used by a forwarder's outbound delivery (ADR-047 §2.12).
115
+ # HTTP verb used by a forwarder's outbound delivery.
108
116
  #
109
117
  # Mirrors the audit spec's +HttpConfigurationMethod+ enum so the
110
118
  # +HttpConfiguration#method+ field is constrained to a known value
@@ -135,11 +143,10 @@ module Smplkit
135
143
  end
136
144
  end
137
145
 
138
- # Engine that evaluates a forwarder's +transform+ template
139
- # (ADR-047 §2.12). Only +JSONATA+ is supported today; the enum
140
- # exists so the field is typed instead of accepting any string,
141
- # and so additional engines can be added without breaking the
142
- # public surface.
146
+ # Engine that evaluates a forwarder's +transform+ template.
147
+ # Only +JSONATA+ is supported today; the enum exists so the field is
148
+ # typed instead of accepting any string, and so additional engines
149
+ # can be added without breaking the public surface.
143
150
  module TransformType
144
151
  JSONATA = "JSONATA"
145
152
 
@@ -161,7 +168,7 @@ module Smplkit
161
168
  end
162
169
  end
163
170
 
164
- # A single audit event as returned by the audit service (ADR-047 §2.3.1).
171
+ # A single audit event as returned by the audit service.
165
172
  #
166
173
  # @!attribute [rw] id
167
174
  # @return [String] Server-assigned UUID for this event.
@@ -181,6 +188,11 @@ module Smplkit
181
188
  # @return [String, nil] Customer-supplied free-form actor identifier — +nil+ when not provided.
182
189
  # @!attribute [rw] actor_label
183
190
  # @return [String, nil] Customer-supplied display label for the actor — typically a name or email.
191
+ # @!attribute [rw] category
192
+ # @return [String, nil] Free-form bucket label for the event — e.g.
193
+ # +"auth"+, +"billing"+, +"config-change"+. Stored exactly as supplied;
194
+ # drives the audit log's category filter and the +categories+ discovery
195
+ # listing ({Smplkit::Audit::AuditClient#categories}). +nil+ when not supplied.
184
196
  # @!attribute [rw] data
185
197
  # @return [Hash{String => Object}] Free-form per-event payload defined by the customer.
186
198
  # @!attribute [rw] idempotency_key
@@ -196,7 +208,7 @@ module Smplkit
196
208
  AuditEvent = Struct.new(
197
209
  :id, :event_type, :resource_type, :resource_id,
198
210
  :occurred_at, :created_at,
199
- :actor_type, :actor_id, :actor_label,
211
+ :actor_type, :actor_id, :actor_label, :category,
200
212
  :data, :idempotency_key, :do_not_forward, :environment,
201
213
  keyword_init: true
202
214
  ) do
@@ -212,6 +224,7 @@ module Smplkit
212
224
  actor_type: attrs.actor_type,
213
225
  actor_id: attrs.actor_id,
214
226
  actor_label: attrs.actor_label,
227
+ category: attrs.category,
215
228
  data: Smplkit::Helpers.deep_stringify_keys(attrs.data || {}),
216
229
  idempotency_key: attrs.idempotency_key,
217
230
  do_not_forward: attrs.do_not_forward || false,
@@ -223,9 +236,9 @@ module Smplkit
223
236
  # A distinct +resource_type+ slug seen for the account.
224
237
  #
225
238
  # The +id+ and +resource_type+ are the same value — JSON:API surfaces
226
- # the customer-facing key as the resource id (ADR-014). The duplication
227
- # keeps SDK consumers from having to dig into the id field when
228
- # filtering UI controls; pick whichever name reads better in context.
239
+ # the customer-facing key as the resource id. The duplication keeps SDK
240
+ # consumers from having to dig into the id field when filtering UI
241
+ # controls; pick whichever name reads better in context.
229
242
  #
230
243
  # @!attribute [rw] id
231
244
  # @return [String] JSON:API resource id (same as +resource_type+).
@@ -272,8 +285,8 @@ module Smplkit
272
285
  #
273
286
  # Same shape as {ResourceType}/{EventType} — +id+ and +category+ are the
274
287
  # same value (JSON:API surfaces the customer-facing key as the resource
275
- # id, ADR-014). +created_at+ is the earliest sighting of this category
276
- # for the account.
288
+ # id). +created_at+ is the earliest sighting of this category for the
289
+ # account.
277
290
  #
278
291
  # @!attribute [rw] id
279
292
  # @return [String] JSON:API resource id (same as +category+).
@@ -297,8 +310,8 @@ module Smplkit
297
310
  # @!attribute [rw] name
298
311
  # @return [String] Header name (e.g. +"Authorization"+, +"DD-API-KEY"+).
299
312
  # @!attribute [rw] value
300
- # @return [String] Header value, plaintext on writes. The audit service
301
- # encrypts values at rest; reads return them as +"<redacted>"+.
313
+ # @return [String] Header value. Returned in plaintext on reads, so a
314
+ # get-mutate-put round-trip preserves it without re-entering secrets.
302
315
  HttpHeader = Struct.new(:name, :value, keyword_init: true)
303
316
 
304
317
  # Forwarder destination HTTP request shape.
@@ -309,8 +322,9 @@ module Smplkit
309
322
  # @return [String] Destination URL the audit service sends each event to.
310
323
  # @!attribute [rw] headers
311
324
  # @return [Array<HttpHeader>] Headers attached to every outbound request.
312
- # Values carry credentials and are encrypted at rest server-side; reads
313
- # return them redacted.
325
+ # Values often carry credentials and are returned in plaintext on
326
+ # reads, so a get-mutate-put round-trip preserves them without
327
+ # re-entering secrets.
314
328
  # @!attribute [rw] success_status
315
329
  # @return [String] Status the destination must return for delivery to count
316
330
  # as success — an exact code (+"200"+, +"204"+) or a class (+"2xx"+, +"4xx"+).
@@ -401,8 +415,8 @@ module Smplkit
401
415
  # configuration that fully replaces the forwarder's base
402
416
  # {Forwarder#configuration} for this environment. +nil+ (the default)
403
417
  # inherits the base configuration. As with the base configuration,
404
- # header values are plaintext on writes and returned redacted on reads —
405
- # re-supply real values before {Forwarder#save}.
418
+ # header values are returned in plaintext on reads, so a get-mutate-put
419
+ # round-trip preserves them without re-entering secrets.
406
420
  ForwarderEnvironment = Struct.new(:enabled, :configuration, keyword_init: true) do
407
421
  def initialize(enabled: false, configuration: nil)
408
422
  super
@@ -422,12 +436,11 @@ module Smplkit
422
436
  # A SIEM streaming forwarder configured on the customer's account.
423
437
  #
424
438
  # Active-record style: instantiate via
425
- # +mgmt.audit.forwarders.new_forwarder(...)+, mutate fields directly,
439
+ # +client.audit.forwarders.new(...)+, mutate fields directly,
426
440
  # and call {#save} to persist or {#delete} to remove. Header values in
427
- # +configuration.headers+ are returned redacted on reads the GET path
428
- # on the audit API replaces every header value with +"<redacted>"+.
429
- # Re-supply real values before calling {#save}; the SDK does not cache
430
- # them client-side.
441
+ # +configuration.headers+ are returned in plaintext on reads, so fetching
442
+ # a forwarder, mutating it, and calling {#save} preserves its header
443
+ # values without re-entering secrets.
431
444
  class Forwarder
432
445
  # @return [String, nil] Caller-supplied unique identifier (key) for this
433
446
  # forwarder. Unique within an account; immutable for the lifetime of
@@ -494,7 +507,7 @@ module Smplkit
494
507
  # @return [String, nil] ISO-8601 timestamp of the most recent mutation.
495
508
  attr_accessor :updated_at
496
509
 
497
- # @return [String, nil] Soft-delete timestamp. +nil+ for live forwarders.
510
+ # @return [String, nil] Deletion timestamp; +nil+ for live forwarders.
498
511
  attr_accessor :deleted_at
499
512
 
500
513
  # @return [Integer, nil] Monotonic version counter, bumped on every server-side write.
@@ -548,7 +561,7 @@ module Smplkit
548
561
  end
549
562
  alias save! save
550
563
 
551
- # Soft-delete this forwarder on the server.
564
+ # Delete this forwarder on the server.
552
565
  #
553
566
  # @return [nil]
554
567
  def delete
@@ -558,6 +571,47 @@ module Smplkit
558
571
  end
559
572
  alias delete! delete
560
573
 
574
+ # Set this forwarder's destination configuration in memory.
575
+ #
576
+ # With +environment+ omitted, replaces the base {#configuration}. With
577
+ # +environment+ given, sets the per-environment override's configuration
578
+ # on {#environments}, creating the override entry if it doesn't exist yet
579
+ # (preserving any already-set +enabled+ on it). Call {#save} to persist.
580
+ def set_configuration(configuration, environment: nil)
581
+ if environment.nil?
582
+ @configuration = configuration
583
+ else
584
+ _environment_override(environment).configuration = configuration
585
+ end
586
+ end
587
+
588
+ # Set this forwarder's enablement in memory.
589
+ #
590
+ # With +environment+ omitted, sets the base {#enabled} (which the server
591
+ # pins false regardless — enablement is per-environment). With
592
+ # +environment+ given, sets the per-environment override's +enabled+ on
593
+ # {#environments}, creating the override entry if it doesn't exist yet
594
+ # (preserving any already-set +configuration+ on it). Call {#save} to
595
+ # persist.
596
+ def set_enabled(enabled, environment: nil)
597
+ if environment.nil?
598
+ @enabled = enabled
599
+ else
600
+ _environment_override(environment).enabled = enabled
601
+ end
602
+ end
603
+
604
+ # Return the override for +environment+, creating an empty one if absent.
605
+ #
606
+ # The per-environment mutators reach through here so an existing
607
+ # override's other field is preserved when only one of +enabled+ /
608
+ # +configuration+ is being set.
609
+ #
610
+ # @api private
611
+ def _environment_override(environment)
612
+ @environments[environment] ||= ForwarderEnvironment.new
613
+ end
614
+
561
615
  # @api private
562
616
  def _apply(other)
563
617
  @id = other.id
@@ -5,20 +5,32 @@ module Smplkit
5
5
  # +client.audit.resource_types.list+ — distinct +resource_type+ slugs
6
6
  # seen for the account.
7
7
  #
8
- # Backed by a maintain-by-write side table (ADR-047 §2.5), so the
9
- # response time is independent of how many years of events the
10
- # account has accumulated. Sorted alphabetically; offset pagination
11
- # (+page_number+ / +page_size+) per ADR-014.
8
+ # Response time is independent of how many years of events the account has
9
+ # accumulated. Sorted alphabetically; offset paginated.
12
10
  class ResourceTypes
13
11
  def initialize(api)
14
12
  @api = api
15
13
  end
16
14
 
17
- # +environments+ is an optional array of environment keys (and/or the
18
- # reserved +"smplkit"+ control-plane bucket) used to scope the read;
19
- # the values are comma-joined into +filter[environment]+. Omitting it
20
- # (or passing an empty array) leaves the filter unset — identical to
21
- # the prior behavior on the wire.
15
+ # List the distinct +resource_type+ slugs seen in the account.
16
+ #
17
+ # +environments+ scopes the listing to a set of environments: pass an
18
+ # array of environment keys and/or the reserved +"smplkit"+ control-plane
19
+ # bucket; the values are comma-joined into +filter[environment]+. Omitting
20
+ # it (or passing an empty array) leaves the filter off entirely.
21
+ #
22
+ # @param page_number [Integer, nil] 1-based page index. Omit for the first
23
+ # page.
24
+ # @param page_size [Integer, nil] Maximum number of slugs to return in this
25
+ # page.
26
+ # @param meta_total [Boolean, nil] When +true+, populate +total+ and
27
+ # +total_pages+ in the returned page's +pagination+ block (costs an extra
28
+ # count server-side). Omit to skip it.
29
+ # @param environments [Array<String>, nil] Environment keys and/or the
30
+ # reserved +"smplkit"+ control-plane bucket to scope the listing to. Omit
31
+ # to leave the filter off entirely.
32
+ # @return [Smplkit::Audit::ResourceTypeListPage] A page of the matching
33
+ # resource-type slugs.
22
34
  def list(page_number: nil, page_size: nil, meta_total: nil, environments: nil)
23
35
  opts = {}
24
36
  opts[:page_number] = page_number if page_number