smplkit 3.0.96 → 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.
@@ -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
@@ -424,10 +438,9 @@ module Smplkit
424
438
  # Active-record style: instantiate via
425
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
@@ -593,6 +606,8 @@ module Smplkit
593
606
  # The per-environment mutators reach through here so an existing
594
607
  # override's other field is preserved when only one of +enabled+ /
595
608
  # +configuration+ is being set.
609
+ #
610
+ # @api private
596
611
  def _environment_override(environment)
597
612
  @environments[environment] ||= ForwarderEnvironment.new
598
613
  end
@@ -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
@@ -15,17 +15,26 @@ require "set"
15
15
  module Smplkit
16
16
  # When the deduplication LRU exceeds this size, the oldest entry is
17
17
  # evicted. The next observation of an evicted entry will re-flush.
18
+ #
19
+ # @api private
18
20
  CONTEXT_REGISTRATION_LRU_SIZE = 10_000
19
21
 
20
22
  # Pending-queue size that triggers an immediate background flush from
21
23
  # inside +register+. The periodic timer on +Client+ covers the tail
22
24
  # case for low-traffic services.
25
+ #
26
+ # @api private
23
27
  CONTEXT_BATCH_FLUSH_SIZE = 100
28
+ # @api private
24
29
  FLAG_BATCH_FLUSH_SIZE = 50
30
+ # @api private
25
31
  LOGGER_BATCH_FLUSH_SIZE = 50
32
+ # @api private
26
33
  CONFIG_BATCH_FLUSH_SIZE = 50
27
34
 
28
35
  # Thread-safe batch buffer for context registration.
36
+ #
37
+ # @api private
29
38
  class ContextRegistrationBuffer
30
39
  def initialize
31
40
  @seen = {}
@@ -66,6 +75,8 @@ module Smplkit
66
75
  # Use +peek+ + +commit(ids)+ for the send path so a failed POST leaves
67
76
  # declarations queued for the next attempt; the legacy +drain+ is
68
77
  # unconditional and used only by tests.
78
+ #
79
+ # @api private
69
80
  class FlagRegistrationBuffer
70
81
  def initialize
71
82
  @seen = {}
@@ -131,6 +142,8 @@ module Smplkit
131
142
  # the customer's code declares new items via typed getters after a flush, a
132
143
  # fresh pending entry is created using the stored metadata so the server can
133
144
  # route the delta to the right source row.
145
+ #
146
+ # @api private
134
147
  class ConfigRegistrationBuffer
135
148
  def initialize
136
149
  @pending = {} # config_id -> { id:, items: {}, ...meta }
@@ -200,6 +213,8 @@ module Smplkit
200
213
  end
201
214
 
202
215
  # Thread-safe batch buffer for logger discovery.
216
+ #
217
+ # @api private
203
218
  class LoggerRegistrationBuffer
204
219
  def initialize
205
220
  @seen = {}
@@ -18,9 +18,11 @@ module Smplkit
18
18
  # # ...
19
19
  # end
20
20
  #
21
- # All parameters are optional. When omitted, the SDK resolves them from
22
- # environment variables (+SMPLKIT_*+) or the +~/.smplkit+ configuration file.
23
- # See ADR-021 for the full resolution algorithm.
21
+ # All parameters are optional. When omitted, the SDK resolves each one in
22
+ # precedence order, lowest to highest: built-in defaults, then the
23
+ # +~/.smplkit+ configuration file, then +SMPLKIT_*+ environment variables,
24
+ # then the explicit constructor arguments (a value supplied at a higher
25
+ # level overrides the lower ones).
24
26
  #
25
27
  # +Smplkit::Client+ is thread-safe by construction. Background work runs on
26
28
  # internal SDK-owned threads; public methods block the calling thread and
@@ -33,7 +35,11 @@ module Smplkit
33
35
 
34
36
  attr_reader :platform, :account, :config, :flags, :logging, :audit, :jobs
35
37
 
36
- # Construct, yield to the block, and close on exit.
38
+ # Construct a client, yield it to the block, and close it on exit.
39
+ #
40
+ # @param kwargs [Hash] The same keyword arguments as {#initialize}.
41
+ # @yieldparam client [Client] The constructed client.
42
+ # @return [Object] The block's return value.
37
43
  def self.open(**kwargs)
38
44
  client = new(**kwargs)
39
45
  begin
@@ -43,6 +49,16 @@ module Smplkit
43
49
  end
44
50
  end
45
51
 
52
+ # @param api_key [String, nil] API key for authenticating with the smplkit platform.
53
+ # @param environment [String, nil] The environment to connect to (e.g. +"production"+).
54
+ # @param service [String, nil] Service name (e.g. +"user-service"+).
55
+ # @param profile [String, nil] Named profile section to read from +~/.smplkit+.
56
+ # @param base_domain [String, nil] Base domain for API requests (default +"smplkit.com"+).
57
+ # @param scheme [String, nil] URL scheme (default +"https"+).
58
+ # @param debug [Boolean, nil] Enable debug logging in the SDK.
59
+ # @param telemetry [Boolean, nil] Enable anonymous usage telemetry (default +true+).
60
+ # @param extra_headers [Hash{String => String}, nil] Extra HTTP headers attached to
61
+ # every request the client sends.
46
62
  def initialize(api_key: nil, environment: nil, service: nil, profile: nil,
47
63
  base_domain: nil, scheme: nil, debug: nil, telemetry: nil,
48
64
  extra_headers: nil)
@@ -144,6 +160,12 @@ module Smplkit
144
160
  # connected here — call +client.logging.install+ separately if you want it
145
161
  # (it installs adapters and hooks into your application's logger, which
146
162
  # should be opt-in).
163
+ #
164
+ # @param timeout [Float] Maximum seconds to wait for the live-updates
165
+ # WebSocket handshake before giving up. Defaults to +10.0+.
166
+ # @return [void]
167
+ # @raise [Smplkit::TimeoutError] If the WebSocket fails to connect within
168
+ # +timeout+ seconds.
147
169
  def wait_until_ready(timeout: 10.0)
148
170
  @flags._ensure_connected
149
171
  @config._ensure_connected
@@ -165,8 +187,8 @@ module Smplkit
165
187
  # every +flag.get+ (and other context-sensitive evaluations) inside that
166
188
  # request automatically picks it up.
167
189
  #
168
- # Each unique +(type, key)+ is also queued for bulk registration on the
169
- # management API (deduplicated; flushed in the background).
190
+ # Each unique +(type, key)+ is also registered with the platform
191
+ # (deduplicated via an LRU; sent in the background).
170
192
  #
171
193
  # Two usage shapes:
172
194
  #
@@ -178,6 +200,14 @@ module Smplkit
178
200
  # # ...
179
201
  # end
180
202
  # # original context restored here
203
+ #
204
+ # @param contexts [Array<Smplkit::Context>] The contexts to make active for
205
+ # the current thread (e.g. the request's user and account). An empty array
206
+ # clears any registration step but still returns a scope.
207
+ # @yield When a block is given, the contexts are active only for its
208
+ # duration and the previous context is restored on exit.
209
+ # @return [Object, Smplkit::ContextScope] The block's return value when a
210
+ # block is given; otherwise a scope you can ignore for fire-and-forget use.
181
211
  def set_context(contexts, &block)
182
212
  _ensure_started
183
213
  @platform.contexts.register(contexts) if contexts && !contexts.empty?
@@ -191,6 +221,8 @@ module Smplkit
191
221
  end
192
222
 
193
223
  # Release all resources held by this client.
224
+ #
225
+ # @return [void]
194
226
  def close
195
227
  Smplkit.debug("lifecycle", "Client.close called")
196
228
  @closed = true