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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: daa3b1ae6cdf8922badb57509c04e16d007372f82b95661de9f223602f174a2b
4
- data.tar.gz: d03104fec0c8f27c9d5942821b8c49143d6321f4b9790da7be91f7a2aef55ddf
3
+ metadata.gz: c317a0f573d990742547e60f7e17bba0bd65a6abc13dae8bcf8ab13ca7e167e3
4
+ data.tar.gz: c02f2385189d409d3f1a8263156f0a659e55ae762d4c2f9940bdf6a51d61f19d
5
5
  SHA512:
6
- metadata.gz: 86ef3645c6d7c44b4afd90ac2948762a3ff4d2d2df6076fb524f6f636cac2d0b0335d55e4bd3a6fac974933f65f332a2754eac9ac417d2aff91ab4b7bc57c874
7
- data.tar.gz: 786ad2e7704b39d79090c51b6fc4663e6072fda4e1e3d51fb70abbb272f951a17246351a7a0600c4e56d9c696c82f486f2fda490398a9e0e4aa0e1be4c728c1e
6
+ metadata.gz: 33f03cf3bde2e7a38b9856b558be13546c374ceddec2c02ec9cb5d454922f373d46f39188e86ffbbba194421318b9d04bba1a966ac2930ad088c6cf46f6d7cf5
7
+ data.tar.gz: 91337cdc5b3da03eae931e8b67b7454d0d04925e667446b49ba003ecd0c6bcd091f319f15df58d2c926f818a563a5ab4648ef769261ffacc9422636f125dd3a6
@@ -0,0 +1,128 @@
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 config resolver fills in whatever is missing.
13
+ #
14
+ # @api private
15
+ def self.resolve_account_target(api_key:, base_url:, profile:, base_domain:, scheme:, debug:, extra_headers:)
16
+ cfg = ConfigResolution.resolve_client_config(
17
+ profile: profile, api_key: api_key, base_domain: base_domain, scheme: scheme, debug: debug
18
+ )
19
+ resolved_key = api_key.nil? ? cfg.api_key : api_key
20
+ app_url = base_url.nil? ? ConfigResolution.service_url(cfg.scheme, "app", cfg.base_domain) : base_url
21
+ headers = {}
22
+ headers.merge!(cfg.extra_headers || {})
23
+ headers.merge!(extra_headers || {})
24
+ [app_url.sub(%r{/+\z}, ""), resolved_key, headers]
25
+ end
26
+
27
+ # Sync account-settings get/save (+client.account.settings+).
28
+ #
29
+ # The endpoint isn't JSON:API — body is a raw JSON object — so we use an
30
+ # HTTP client directly rather than going through a generated client.
31
+ class SettingsClient
32
+ SETTINGS_PATH = "/api/v1/accounts/current/settings"
33
+
34
+ def initialize(app_base_url, api_key, extra_headers = nil)
35
+ @base_url = app_base_url
36
+ @headers = {
37
+ "Authorization" => "Bearer #{api_key}",
38
+ "Content-Type" => "application/json"
39
+ }.merge(extra_headers || {})
40
+ end
41
+
42
+ # Fetch the authenticated account's current settings.
43
+ #
44
+ # @return [Smplkit::Account::AccountSettings] An active record. Mutate its
45
+ # fields and call +save+ to persist the changes.
46
+ def get
47
+ resp = connection.get(SETTINGS_PATH)
48
+ Errors.raise_for_status(resp.status, resp.body.to_s)
49
+ AccountSettings.new(self, data: parse_body(resp.body))
50
+ end
51
+
52
+ # @api private
53
+ def _save(data)
54
+ resp = connection.put(SETTINGS_PATH) { |req| req.body = JSON.generate(data) }
55
+ Errors.raise_for_status(resp.status, resp.body.to_s)
56
+ AccountSettings.new(self, data: parse_body(resp.body))
57
+ end
58
+
59
+ private
60
+
61
+ # A short-lived connection per call — mirrors the Python settings client
62
+ # opening and closing its own HTTP client each time.
63
+ def connection
64
+ Faraday.new(url: @base_url, headers: @headers, request: { timeout: 30 })
65
+ end
66
+
67
+ def parse_body(body)
68
+ return {} if body.nil? || body.to_s.empty?
69
+
70
+ parsed = JSON.parse(body)
71
+ parsed.is_a?(Hash) ? parsed : {}
72
+ rescue JSON::ParserError
73
+ {}
74
+ end
75
+ end
76
+
77
+ # The Smpl Account client (sync).
78
+ #
79
+ # Exposes the authenticated account's own configuration, reachable as
80
+ # +client.account+ (+Smplkit::Client+) or constructed directly:
81
+ #
82
+ # account = Smplkit::AccountClient.new(api_key: "sk_...")
83
+ # settings = account.settings.get
84
+ # settings.environment_order = ["production", "staging"]
85
+ # settings.save
86
+ #
87
+ # Sub-client: +settings+ (get/save). Pure CRUD — no +install+ required.
88
+ #
89
+ # @param api_key [String, nil] API key. When omitted, resolved from
90
+ # +SMPLKIT_API_KEY+ or +~/.smplkit+.
91
+ # @param base_url [String, nil] Full app-service base URL. Usually resolved
92
+ # from +base_domain+/+scheme+; supplied directly by the top-level clients
93
+ # which have already computed it.
94
+ # @param profile [String, nil] Named +~/.smplkit+ profile section.
95
+ # @param base_domain [String, nil] Base domain for API requests (default
96
+ # +"smplkit.com"+).
97
+ # @param scheme [String, nil] URL scheme (default +"https"+).
98
+ # @param debug [Boolean, nil] Enable SDK debug logging.
99
+ # @param extra_headers [Hash, nil] Extra headers attached to every request.
100
+ class AccountClient
101
+ attr_reader :settings
102
+
103
+ def initialize(api_key: nil, base_url: nil, profile: nil, base_domain: nil,
104
+ scheme: nil, debug: nil, extra_headers: nil)
105
+ app_url, resolved_key, headers = Account.resolve_account_target(
106
+ api_key: api_key, base_url: base_url, profile: profile,
107
+ base_domain: base_domain, scheme: scheme, debug: debug, extra_headers: extra_headers
108
+ )
109
+ @settings = SettingsClient.new(app_url, resolved_key, headers.empty? ? nil : headers)
110
+ end
111
+
112
+ # No-op — the settings client opens a short-lived HTTP client per call.
113
+ def close; end
114
+
115
+ # Construct, yield to the block, and close on exit.
116
+ def self.open(**kwargs)
117
+ client = new(**kwargs)
118
+ begin
119
+ yield client
120
+ ensure
121
+ client.close
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ AccountClient = Account::AccountClient
128
+ end
@@ -0,0 +1,71 @@
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
+ #
18
+ # @return [Hash] The full settings dict.
19
+ def raw
20
+ @data
21
+ end
22
+
23
+ # Replace the full settings dict.
24
+ #
25
+ # @param value [Hash] The new settings dict.
26
+ # @return [void]
27
+ def raw=(value)
28
+ @data = value.dup
29
+ end
30
+
31
+ # Canonical ordering of STANDARD environments. Empty list if unset.
32
+ #
33
+ # @return [Array<String>] The environment ids in canonical order.
34
+ def environment_order
35
+ Array(@data["environment_order"] || [])
36
+ end
37
+
38
+ # Set the canonical ordering of STANDARD environments.
39
+ #
40
+ # @param value [Array<String>] The environment ids in canonical order.
41
+ # @return [void]
42
+ def environment_order=(value)
43
+ @data["environment_order"] = value.to_a
44
+ end
45
+
46
+ # Write the full settings object back to the account.
47
+ #
48
+ # @return [AccountSettings] +self+, updated with the server response.
49
+ # @raise [RuntimeError] If this model was constructed without a client
50
+ # (e.g. built by hand rather than returned from +get+).
51
+ def save
52
+ raise "AccountSettings was constructed without a client; cannot save" if @client.nil?
53
+
54
+ other = @client._save(@data)
55
+ _apply(other)
56
+ self
57
+ end
58
+ alias save! save
59
+
60
+ def to_s
61
+ "AccountSettings(#{@data.inspect})"
62
+ end
63
+ alias inspect to_s
64
+
65
+ # @api private
66
+ def _apply(other)
67
+ @data = other.raw.dup
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ # Ruby-internal adapters bridging the generated client layer to the wrapper.
5
+ #
6
+ # @api private
7
+ module ApiSupport
8
+ # Default page[size] the runtime asks for when walking a list endpoint to
9
+ # completion. The platform caps page[size] at 1000; using the same value
10
+ # here makes the minimum number of round-trips per exhaustive fetch.
11
+ RUNTIME_PAGE_SIZE = 1000
12
+
13
+ # Wraps a generated-API call and converts any +ApiError+ raised by the
14
+ # generated layer into the +Smplkit::Error+ hierarchy. Connection-level
15
+ # failures (no response from the server) become +Smplkit::ConnectionError+;
16
+ # status-coded failures route through +Errors.raise_for_status+ which emits
17
+ # +NotFoundError+ / +ConflictError+ / +ValidationError+ / +Error+ depending
18
+ # on the JSON:API body.
19
+ #
20
+ # @api private
21
+ module ErrorMapping
22
+ module_function
23
+
24
+ def call
25
+ yield
26
+ rescue StandardError => e
27
+ raise unless generated_api_error?(e)
28
+
29
+ raise Smplkit::ConnectionError, e.message.to_s if e.code.nil? || e.code.zero?
30
+
31
+ Smplkit::Errors.raise_for_status(e.code, e.response_body.to_s)
32
+ # raise_for_status only returns on 2xx; if we get here the generated
33
+ # layer raised on a 2xx (shouldn't happen) so re-raise the original.
34
+ raise
35
+ end
36
+
37
+ def generated_api_error?(err)
38
+ klass_name = err.class.name.to_s
39
+ klass_name.start_with?("SmplkitGeneratedClient::") && klass_name.end_with?("::ApiError")
40
+ end
41
+ end
42
+
43
+ # Walk a generated paginated list endpoint to completion.
44
+ #
45
+ # The block receives a per-page +opts+ hash with +page_number+ and
46
+ # +page_size+ filled in, calls the generated list method through
47
+ # {ErrorMapping.call}, and returns the response object. Pages stop when the
48
+ # server returns fewer rows than requested — the platform's standard
49
+ # last-page signal across every offset-paginated list endpoint. Returns the
50
+ # concatenated +response.data+ rows.
51
+ #
52
+ # @api private
53
+ module PaginatedFetch
54
+ module_function
55
+
56
+ def collect(page_size: RUNTIME_PAGE_SIZE)
57
+ rows = []
58
+ page_number = 1
59
+ loop do
60
+ opts = { page_number: page_number, page_size: page_size }
61
+ response = ErrorMapping.call { yield(opts) }
62
+ page = response.data || []
63
+ rows.concat(page)
64
+ break if page.length < page_size
65
+
66
+ page_number += 1
67
+ end
68
+ rows
69
+ end
70
+ end
71
+
72
+ # Deep-stringify Hash keys so resources returned by generated +to_hash+
73
+ # (symbol-keyed) match what the wrapper helpers expect (string-keyed).
74
+ #
75
+ # @api private
76
+ module ResourceShim
77
+ module_function
78
+
79
+ def stringify(value)
80
+ Smplkit::Helpers.deep_stringify_keys(value)
81
+ end
82
+
83
+ # Convenience: produce a string-keyed Hash from a generated model.
84
+ def from_model(model)
85
+ return {} if model.nil?
86
+
87
+ stringify(model.to_hash)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -3,13 +3,15 @@
3
3
  module Smplkit
4
4
  module Audit
5
5
  # Bounded in-memory queue + worker thread for fire-and-forget audit
6
- # emits (ADR-047 §2.6).
6
+ # emits.
7
7
  #
8
8
  # +#enqueue+ returns immediately. The worker drains on either a
9
9
  # periodic tick or once depth crosses the high-water mark, retries
10
10
  # transient failures with exponential backoff, drops permanent 4xx
11
11
  # (other than 429), and evicts the oldest item under sustained
12
12
  # back-pressure.
13
+ #
14
+ # @api private
13
15
  class EventBuffer
14
16
  MAX_BUFFER_SIZE = 1000
15
17
  WATERMARK = 50
@@ -5,21 +5,32 @@ module Smplkit
5
5
  # +client.audit.categories.list+ — distinct +category+ values seen for
6
6
  # the account.
7
7
  #
8
- # Backed by a maintain-by-write side table populated whenever an event
9
- # is recorded with a non-null +category+ (ADR-047 §2.5), so the response
10
- # time is independent of how many years of events the account has
11
- # accumulated. Sorted alphabetically; offset pagination (+page_number+ /
12
- # +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.
13
10
  class Categories
14
11
  def initialize(api)
15
12
  @api = api
16
13
  end
17
14
 
18
- # +environments+ is an optional array of environment keys (and/or the
19
- # reserved +"smplkit"+ control-plane bucket) used to scope the read;
20
- # the values are comma-joined into +filter[environment]+. Omitting it
21
- # (or passing an empty array) leaves the filter unset — identical to
22
- # the prior behavior on the wire.
15
+ # List the distinct +category+ values 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 categories to return in
25
+ # this 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::CategoryListPage] A page of the matching
33
+ # category values.
23
34
  def list(page_number: nil, page_size: nil, meta_total: nil, environments: nil)
24
35
  opts = {}
25
36
  opts[:page_number] = page_number if page_number
@@ -2,18 +2,26 @@
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.
7
+ # One client exposes the full surface — event recording and reads,
8
+ # distinct-value discovery, and SIEM forwarder CRUD. Owns fire-and-forget
9
+ # +#events.record+, plus the audit-log +list+ / +get+ and the distinct-value
10
+ # listings (+resource_types+, +event_types+, +categories+), plus SIEM
11
+ # forwarder CRUD on +#forwarders+.
12
12
  #
13
- # SIEM forwarder CRUD lives on {Smplkit::ManagementClient} under
14
- # +mgmt.audit.forwarders.*+.
13
+ # @param api_key [String] API key used to authenticate every request.
14
+ # @param base_url [String] Full audit-service base URL.
15
+ # @param environment [String, nil] Deployment environment to scope recording
16
+ # and reads to. Optional — forwarder CRUD and discovery are
17
+ # environment-agnostic, and reads accept an explicit +environments: [...]+
18
+ # filter.
19
+ # @param timeout [Float] Per-request timeout, in seconds. Defaults to +10.0+.
20
+ # @param extra_headers [Hash{String => String}, nil] Extra headers attached
21
+ # to every request. SDK-owned headers (authorization, content-type,
22
+ # user-agent) cannot be overridden.
15
23
  class AuditClient
16
- attr_reader :events, :resource_types, :event_types, :categories
24
+ attr_reader :events, :resource_types, :event_types, :categories, :forwarders
17
25
 
18
26
  SDK_OWNED_HEADERS = %w[authorization content-type user-agent].freeze
19
27
 
@@ -41,6 +49,7 @@ module Smplkit
41
49
  @resource_types = ResourceTypes.new(SmplkitGeneratedClient::Audit::ResourceTypesApi.new(api_client))
42
50
  @event_types = EventTypes.new(SmplkitGeneratedClient::Audit::EventTypesApi.new(api_client))
43
51
  @categories = Categories.new(SmplkitGeneratedClient::Audit::CategoriesApi.new(api_client))
52
+ @forwarders = ForwardersClient.new(SmplkitGeneratedClient::Audit::ForwardersApi.new(api_client))
44
53
  end
45
54
 
46
55
  def _close
@@ -7,22 +7,38 @@ module Smplkit
7
7
  #
8
8
  # Without +filter_resource_type+, returns one row per distinct
9
9
  # event type — an event type recorded with multiple resource_types appears
10
- # once. With the filter, returns the event types seen with that
11
- # specific resource_type, powering the cascading-filter behavior
12
- # on the Activity tab.
10
+ # once. With the filter, returns the event types seen with that specific
11
+ # resource_type, which supports building a cascading resource-type-then-
12
+ # event-type filter.
13
13
  #
14
- # ADR-047 §2.5. Sorted alphabetically; offset pagination
15
- # (+page_number+ / +page_size+) per ADR-014.
14
+ # Sorted alphabetically; offset paginated.
16
15
  class EventTypes
17
16
  def initialize(api)
18
17
  @api = api
19
18
  end
20
19
 
21
- # +environments+ is an optional array of environment keys (and/or the
22
- # reserved +"smplkit"+ control-plane bucket) used to scope the read;
23
- # the values are comma-joined into +filter[environment]+. Omitting it
24
- # (or passing an empty array) leaves the filter unset — identical to
25
- # the prior behavior on the wire.
20
+ # List the distinct +event_type+ slugs seen in the account.
21
+ #
22
+ # +environments+ scopes the listing to a set of environments: pass an
23
+ # array of environment keys and/or the reserved +"smplkit"+ control-plane
24
+ # bucket; the values are comma-joined into +filter[environment]+. Omitting
25
+ # it (or passing an empty array) leaves the filter off entirely.
26
+ #
27
+ # @param filter_resource_type [String, nil] Restrict the listing to
28
+ # event_types seen with this +resource_type+. Omit to list every distinct
29
+ # event_type.
30
+ # @param page_number [Integer, nil] 1-based page index. Omit for the first
31
+ # page.
32
+ # @param page_size [Integer, nil] Maximum number of slugs to return in this
33
+ # page.
34
+ # @param meta_total [Boolean, nil] When +true+, populate +total+ and
35
+ # +total_pages+ in the returned page's +pagination+ block (costs an extra
36
+ # count server-side). Omit to skip it.
37
+ # @param environments [Array<String>, nil] Environment keys and/or the
38
+ # reserved +"smplkit"+ control-plane bucket to scope the listing to. Omit
39
+ # to leave the filter off entirely.
40
+ # @return [Smplkit::Audit::EventTypeListPage] A page of the matching
41
+ # event-type slugs.
26
42
  def list(filter_resource_type: nil, page_number: nil, page_size: nil, meta_total: nil, environments: nil)
27
43
  opts = {}
28
44
  opts[:filter_resource_type] = filter_resource_type if filter_resource_type
@@ -4,9 +4,9 @@ module Smplkit
4
4
  module Audit
5
5
  # Audit events surface — accessed via +client.audit.events+.
6
6
  #
7
- # +#record+ is fire-and-forget per ADR-047 §2.6 the call enqueues
8
- # the event onto an in-memory bounded buffer and returns
9
- # immediately. +#list+ and +#get+ are synchronous reads.
7
+ # +#record+ is fire-and-forget the call enqueues the event onto an
8
+ # in-memory bounded buffer and returns immediately. +#list+ and +#get+
9
+ # are synchronous reads.
10
10
  class Events
11
11
  def initialize(api)
12
12
  @api = api
@@ -15,18 +15,53 @@ module Smplkit
15
15
 
16
16
  # Enqueue an audit event for asynchronous delivery.
17
17
  #
18
+ # Returns immediately — the buffer's worker thread performs the actual
19
+ # POST with retry on transient failures.
20
+ #
18
21
  # Actor attribution (+actor_type+, +actor_id+, +actor_label+) is
19
22
  # customer-supplied and free-form. The audit service stores
20
23
  # whatever the caller passed and never backfills from the request
21
24
  # credential — supply the fields explicitly when you want the
22
25
  # event attributed.
23
26
  #
24
- # Customer attempts to record events with +resource_type+ starting
25
- # with +smpl.+ are rejected by the server with a 403 (the buffer
26
- # logs and drops permanent failures).
27
+ # @param event_type [String] What happened (e.g. +"invoice.created"+).
28
+ # Any non-empty string.
29
+ # @param resource_type [String] Kind of resource the event is about (e.g.
30
+ # +"invoice"+). Any non-empty string. Customer events must NOT use the
31
+ # +smpl.+ prefix — that namespace is reserved for smplkit-emitted events
32
+ # and the server rejects customer attempts with a 403 (the buffer logs
33
+ # and drops permanent failures).
34
+ # @param resource_id [String] Identifier of the affected resource.
35
+ # @param occurred_at [Time, DateTime, String, nil] When the event happened
36
+ # in the originating system. Defaults to +now+ server-side if omitted.
37
+ # @param actor_type [String, nil] Free-form label for the kind of actor
38
+ # that caused the event (e.g. +"USER"+, +"API_KEY"+, +"SYSTEM"+, or any
39
+ # custom value). The audit service never backfills this from the request
40
+ # credential — supply it explicitly when you want the event attributed.
41
+ # @param actor_id [String, nil] Free-form identifier of the actor that
42
+ # caused the event. Any string scheme is accepted.
43
+ # @param actor_label [String, nil] Human-readable label for the actor
44
+ # (e.g. an email address or API key name).
45
+ # @param category [String, nil] Optional free-form bucket label for the
46
+ # event (e.g. +"auth"+, +"billing"+, +"config-change"+). Stored exactly
47
+ # as supplied; powers the audit log's category filter and the
48
+ # +categories+ discovery listing ({Smplkit::Audit::AuditClient#categories}).
49
+ # Omit it to leave the event uncategorized.
50
+ # @param data [Hash, nil] Free-form contextual JSON. To record a resource
51
+ # snapshot, place it inside +data+ — smplkit's own convention nests it at
52
+ # +data["snapshot"]+ for consistency, but the shape is unconstrained.
53
+ # @param idempotency_key [String, nil] Optional caller-supplied idempotency
54
+ # key. If omitted, the server derives one from event content (account_id
55
+ # + event_type + resource_type + resource_id + occurred_at + actor_* +
56
+ # data).
57
+ # @param do_not_forward [Boolean] When +true+, the audit service records
58
+ # the event normally but does NOT POST it through any configured SIEM
59
+ # forwarder. A +skipped_do_not_forward+ delivery row is recorded for each
60
+ # enabled forwarder so the skip is visible in the forwarder delivery log.
61
+ # @return [void]
27
62
  def record(event_type:, resource_type:, resource_id:,
28
63
  occurred_at: nil, actor_type: nil, actor_id: nil,
29
- actor_label: nil, data: nil, idempotency_key: nil,
64
+ actor_label: nil, category: nil, data: nil, idempotency_key: nil,
30
65
  do_not_forward: false)
31
66
  raise ArgumentError, "event_type is required" if event_type.nil? || event_type.to_s.empty?
32
67
  raise ArgumentError, "resource_type is required" if resource_type.nil? || resource_type.to_s.empty?
@@ -55,6 +90,7 @@ module Smplkit
55
90
  actor_type: actor_type,
56
91
  actor_id: actor_id,
57
92
  actor_label: actor_label,
93
+ category: category,
58
94
  data: data || {},
59
95
  do_not_forward: do_not_forward
60
96
  )
@@ -69,22 +105,55 @@ module Smplkit
69
105
 
70
106
  # Single-event retrieval.
71
107
  #
72
- # Raises {Smplkit::NotFoundError} when no event with that id
73
- # exists in the caller's account.
108
+ # @param event_id [String] The event's UUID, as a string parseable as one.
109
+ # @return [Smplkit::Audit::AuditEvent] The matching event.
110
+ # @raise [Smplkit::NotFoundError] If no event with that id exists in the
111
+ # caller's account.
74
112
  def get(event_id)
75
113
  resp = Smplkit::Audit.call_api { @api.get_event(event_id) }
76
114
  AuditEvent.from_resource(resp.data)
77
115
  end
78
116
 
79
- # List events with filters and cursor pagination. Returns a
80
- # +Smplkit::Audit::ListEventsPage+ whose +#events+ is the page and
81
- # +#next_cursor+ is the opaque token for the next page (or nil).
117
+ # List audit events for the authenticated account.
118
+ #
119
+ # Filters apply server-side. +actor_id+ is matched as a literal string
120
+ # against whatever the recording call stored. Pagination uses an opaque
121
+ # cursor (+page_after+); the returned page exposes +#next_cursor+ when
122
+ # more pages are available.
123
+ #
124
+ # +search+ is an optional free-text filter: pass a string to return only
125
+ # events whose +resource_id+ or +description+ contains it as a
126
+ # case-insensitive substring; omit it (the default) to disable text
127
+ # filtering. A +search+ filter must be scoped — combine it with
128
+ # +occurred_at_range+, or with both +resource_type+ and +resource_id+ —
129
+ # or the request is rejected.
82
130
  #
83
- # +environments+ is an optional array of environment keys (and/or the
84
- # reserved +"smplkit"+ control-plane bucket) used to scope the read; the
85
- # values are comma-joined into +filter[environment]+. Omitting it (or
86
- # passing an empty array) leaves the filter unset — identical to the
87
- # prior behavior on the wire.
131
+ # @param event_type [String, nil] Return only events with this
132
+ # +event_type+. Omit to match any.
133
+ # @param resource_type [String, nil] Return only events about this
134
+ # +resource_type+. Omit to match any.
135
+ # @param resource_id [String, nil] Return only events about this resource
136
+ # id. Omit to match any.
137
+ # @param actor_type [String, nil] Return only events whose +actor_type+
138
+ # equals this value. Omit to match any.
139
+ # @param actor_id [String, nil] Return only events whose +actor_id+
140
+ # matches this value as a literal string. Omit to match any.
141
+ # @param occurred_at_range [String, nil] Restrict to events whose
142
+ # +occurred_at+ falls in this range. Omit to leave the time window open.
143
+ # @param search [String, nil] Optional free-text filter — returns only
144
+ # events whose +resource_id+ or +description+ contains it as a
145
+ # case-insensitive substring. Must be scoped (combine with
146
+ # +occurred_at_range+, or with both +resource_type+ and +resource_id+)
147
+ # or the request is rejected. Omit to disable text filtering.
148
+ # @param environments [Array<String>, nil] Environment keys (and/or the
149
+ # reserved +"smplkit"+ control-plane bucket) to scope the read to. Omit
150
+ # to leave the filter off entirely.
151
+ # @param page_size [Integer, nil] Maximum number of events to return in
152
+ # this page.
153
+ # @param page_after [String, nil] Opaque cursor from a previous page's
154
+ # +next_cursor+. Omit for the first page.
155
+ # @return [Smplkit::Audit::ListEventsPage] A page of the matching events;
156
+ # its +#next_cursor+ is set when more pages are available.
88
157
  def list(event_type: nil, resource_type: nil, resource_id: nil,
89
158
  actor_type: nil, actor_id: nil, occurred_at_range: nil,
90
159
  search: nil, environments: nil, page_size: nil, page_after: nil)
@@ -112,6 +181,13 @@ module Smplkit
112
181
  end
113
182
 
114
183
  # Block until the in-memory buffer is drained or the timeout elapses.
184
+ #
185
+ # Useful for draining buffered events at process shutdown or after a batch
186
+ # of fire-and-forget records.
187
+ #
188
+ # @param timeout [Float, nil] Upper bound on the blocking flush, in
189
+ # seconds. +nil+ blocks indefinitely. Defaults to +5.0+.
190
+ # @return [void]
115
191
  def flush(timeout: 5.0)
116
192
  @buffer.flush(timeout: timeout)
117
193
  end