misardev 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module MisarDev
6
+ # One Server-Sent Event. +data+ is the JSON-decoded payload (string keys), or
7
+ # the raw string when it is not JSON; +event+ and +id+ are the `event:` and
8
+ # `id:` fields when the server sends them.
9
+ StreamEvent = Struct.new(:event, :data, :id, keyword_init: true)
10
+
11
+ module Core
12
+ # Server-Sent Events parsing.
13
+ #
14
+ # Frames are separated by a blank line (\n\n or \r\n\r\n) and may span
15
+ # several `data:` lines, which are joined with newlines per the SSE spec.
16
+ # Comment lines (`:`) are keep-alives and are skipped. The terminator
17
+ # payload — `[DONE]` on the OpenAI-compatible /v1/chat/completions stream —
18
+ # ends iteration and is never yielded. A nil terminator reads to EOF.
19
+ class SSE
20
+ SEPARATOR = /\r?\n\r?\n/.freeze
21
+
22
+ # Parse one complete frame (the text between blank lines).
23
+ # @return [Hash] { event:, data:, id: } — data is nil when the frame had none
24
+ def self.parse_frame(frame)
25
+ event = nil
26
+ id = nil
27
+ data = []
28
+ frame.split(/\r?\n/).each do |line|
29
+ next if line.empty? || line.start_with?(":")
30
+
31
+ idx = line.index(":")
32
+ field = idx ? line[0...idx] : line
33
+ value = idx ? line[(idx + 1)..] : ""
34
+ value = value[1..] if value.start_with?(" ")
35
+ case field
36
+ when "data" then data << value
37
+ when "event" then event = value
38
+ when "id" then id = value
39
+ end
40
+ end
41
+ { event: event, data: data.empty? ? nil : data.join("\n"), id: id }
42
+ end
43
+
44
+ # JSON-decode a payload when possible, else hand back the raw string.
45
+ def self.decode(raw)
46
+ JSON.parse(raw)
47
+ rescue JSON::ParserError
48
+ raw
49
+ end
50
+
51
+ def initialize(terminator = "[DONE]")
52
+ @terminator = terminator
53
+ @buffer = +""
54
+ @done = false
55
+ end
56
+
57
+ # True once the terminator has been seen; stop reading.
58
+ def done?
59
+ @done
60
+ end
61
+
62
+ # Feed a chunk; yields each StreamEvent completed by it.
63
+ def feed(chunk)
64
+ return if @done
65
+
66
+ @buffer << chunk
67
+ while (m = SEPARATOR.match(@buffer))
68
+ frame = @buffer[0...m.begin(0)]
69
+ @buffer = @buffer[m.end(0)..]
70
+ event = to_event(frame)
71
+ return if @done
72
+ next if event.nil?
73
+
74
+ yield event
75
+ end
76
+ end
77
+
78
+ # Flush a final frame the server never terminated with a blank line.
79
+ def finish
80
+ return if @done
81
+
82
+ event = to_event(@buffer)
83
+ @buffer = +""
84
+ yield event if event && !@done
85
+ end
86
+
87
+ private
88
+
89
+ def to_event(frame)
90
+ parsed = self.class.parse_frame(frame)
91
+ return nil if parsed[:data].nil?
92
+
93
+ if @terminator && parsed[:data].strip == @terminator
94
+ @done = true
95
+ return nil
96
+ end
97
+ StreamEvent.new(event: parsed[:event], data: self.class.decode(parsed[:data]), id: parsed[:id])
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ require_relative "../errors"
7
+ require_relative "../version"
8
+ require_relative "http"
9
+ require_relative "sse"
10
+
11
+ module MisarDev
12
+ module Core
13
+ # HTTP transport shared by the generated resource layer.
14
+ #
15
+ # Everything the SDK does goes through here, and it authenticates exactly
16
+ # one way: the account API key (mdk_…), sent as a bearer token. There is no
17
+ # second credential path. What a key may do — and how much of it — is
18
+ # decided server-side from the subscription behind that key; the SDK carries
19
+ # the key and surfaces the answer rather than deciding locally.
20
+ class Transport
21
+ DEFAULT_BASE_URL = "https://api.misar.io/dev"
22
+
23
+ # Statuses worth retrying. 429 only when it is NOT a plan limit.
24
+ RETRYABLE = [429, 500, 502, 503, 504].freeze
25
+
26
+ # A 5xx or a dropped connection on a POST may already have done its work —
27
+ # started a build, charged an allowance. Only requests that are safe to
28
+ # repeat are retried on those; a 429 is always safe because the server
29
+ # refused it before doing anything.
30
+ IDEMPOTENT = %w[GET HEAD OPTIONS PUT DELETE].freeze
31
+
32
+ MAX_BACKOFF = 60.0
33
+
34
+ attr_reader :base_url, :max_retries, :timeout
35
+
36
+ def initialize(api_key = nil, base_url: nil, max_retries: 3, timeout: 60, http_client: nil, headers: {})
37
+ key = present(api_key) || present(ENV["MISARDEV_API_KEY"])
38
+ if key.nil?
39
+ raise MisarDevError.new(
40
+ 0,
41
+ "A MisarDev API key is required. Pass it to the client or set MISARDEV_API_KEY. " \
42
+ "Create one at https://www.misar.dev/settings/api-keys.",
43
+ code: "missing_api_key"
44
+ )
45
+ end
46
+
47
+ @api_key = key
48
+ @base_url = (present(base_url) || present(ENV["MISARDEV_BASE_URL"]) || DEFAULT_BASE_URL).sub(%r{/+\z}, "")
49
+ @max_retries = [1, max_retries.to_i].max
50
+ @timeout = timeout
51
+ @http = http_client || NetHttpClient.new
52
+ @headers = headers || {}
53
+ end
54
+
55
+ def inspect
56
+ "#<#{self.class.name} base_url=#{@base_url.inspect}>"
57
+ end
58
+
59
+ # ── Helpers ───────────────────────────────────────────────────────────
60
+
61
+ UNRESERVED = /[^A-Za-z0-9\-._~]/.freeze
62
+
63
+ # Percent-encode everything but RFC 3986 unreserved characters:
64
+ # "a b/c" → "a%20b%2Fc".
65
+ def self.encode(value)
66
+ value.to_s.b.gsub(UNRESERVED) { |c| format("%%%02X", c.ord) }
67
+ end
68
+
69
+ # Path-segment encoding for a path parameter.
70
+ def self.segment(value)
71
+ encode(value)
72
+ end
73
+
74
+ # Build a query string. nil values are dropped, booleans are sent as
75
+ # true/false, an Array value repeats the key.
76
+ def self.build_query(query)
77
+ return "" if query.nil? || query.empty?
78
+
79
+ pairs = []
80
+ query.each do |key, value|
81
+ (value.is_a?(Array) ? value : [value]).each do |v|
82
+ next if v.nil?
83
+
84
+ pairs << "#{encode(key)}=#{encode(v)}"
85
+ end
86
+ end
87
+ pairs.empty? ? "" : "?#{pairs.join('&')}"
88
+ end
89
+
90
+ # Copy of +body+ with "stream" => true, whatever key style the caller used.
91
+ def self.force_stream(body)
92
+ (body || {}).reject { |k, _| k.to_s == "stream" }.merge("stream" => true)
93
+ end
94
+
95
+ # Seconds to wait before the next attempt: Retry-After when sent, else
96
+ # exponential (250 ms · 2^attempt) with jitter; capped at 60 s.
97
+ def self.backoff(attempt, retry_after = nil)
98
+ hinted = retry_after_seconds(retry_after)
99
+ return [hinted, MAX_BACKOFF].min if hinted
100
+
101
+ base = 0.25 * (2**attempt)
102
+ [base + (rand * base * 0.25), MAX_BACKOFF].min
103
+ end
104
+
105
+ # Parse a Retry-After header — delta seconds or an HTTP-date — into seconds.
106
+ def self.retry_after_seconds(value)
107
+ return nil if value.nil? || value.to_s.strip.empty?
108
+
109
+ s = value.to_s.strip
110
+ if s.match?(/\A\d+(\.\d+)?\z/)
111
+ s.to_f
112
+ else
113
+ [Time.httpdate(s) - Time.now, 0].max.to_f
114
+ end
115
+ rescue ArgumentError
116
+ nil
117
+ end
118
+
119
+ # True when a body is the plan-limit refusal.
120
+ def self.plan_limit_body?(body)
121
+ body.is_a?(Hash) && body["code"] == "plan_limit_exceeded"
122
+ end
123
+
124
+ # Pull a human message out of the API's several error shapes.
125
+ def self.message_of(body, fallback)
126
+ if body.is_a?(Hash)
127
+ return body["error"] if body["error"].is_a?(String)
128
+ # /v1/* answers in the OpenAI shape: { error: { message, type, code } }
129
+ return body["error"]["message"] if body["error"].is_a?(Hash) && body["error"]["message"].is_a?(String)
130
+ return body["message"] if body["message"].is_a?(String)
131
+ end
132
+ return body.strip[0, 500] if body.is_a?(String) && !body.strip.empty?
133
+
134
+ fallback
135
+ end
136
+
137
+ def self.code_of(body)
138
+ return nil unless body.is_a?(Hash)
139
+ return body["code"] if body["code"].is_a?(String)
140
+ return body["error"]["code"] if body["error"].is_a?(Hash) && body["error"]["code"].is_a?(String)
141
+
142
+ nil
143
+ end
144
+
145
+ # Map an error response to the right typed error. Plan limits first, by code.
146
+ def self.error_for(status, headers, body, reason = "")
147
+ headers ||= {}
148
+ request_id = headers["x-request-id"]
149
+ message = message_of(body, reason.to_s.empty? ? "HTTP #{status}" : reason.to_s)
150
+ code = code_of(body)
151
+ common = { code: code, body: body, request_id: request_id }
152
+
153
+ if plan_limit_body?(body)
154
+ return PlanLimitError.new(
155
+ status, message,
156
+ offer: body["upgrade"].is_a?(Hash) ? body["upgrade"] : nil,
157
+ retry_after: retry_after_seconds(headers["retry-after"]),
158
+ body: body, request_id: request_id
159
+ )
160
+ end
161
+
162
+ case status
163
+ when 400, 422 then ValidationError.new(status, message, **common)
164
+ when 401 then AuthenticationError.new(status, message, **common)
165
+ when 403 then PermissionDeniedError.new(status, message, **common)
166
+ when 404 then NotFoundError.new(status, message, **common)
167
+ when 409 then ConflictError.new(status, message, **common)
168
+ when 429 then RateLimitError.new(status, message, retry_after: retry_after_seconds(headers["retry-after"]), **common)
169
+ else
170
+ status >= 500 ? ServerError.new(status, message, **common) : MisarDevError.new(status, message, **common)
171
+ end
172
+ end
173
+
174
+ # 204/empty → nil; JSON → decoded (string keys); anything else → the raw string.
175
+ def self.decode_body(status, text)
176
+ return nil if status == 204 || text.nil? || text.empty?
177
+
178
+ JSON.parse(text)
179
+ rescue JSON::ParserError
180
+ text
181
+ end
182
+
183
+ # ── Requests ──────────────────────────────────────────────────────────
184
+
185
+ # Send one request, retrying throttles and (for idempotent verbs)
186
+ # transient failures.
187
+ #
188
+ # @return [Hash, Array, String, nil] decoded JSON, a raw string, or nil
189
+ # @raise [MisarDevError]
190
+ def request(method, path, query: nil, body: nil, headers: {})
191
+ verb = method.to_s.upcase
192
+ req = Request.new(
193
+ method: verb,
194
+ url: "#{@base_url}#{path}#{self.class.build_query(query)}",
195
+ headers: headers_for("application/json", headers),
196
+ body: encode_body(body),
197
+ timeout: @timeout
198
+ )
199
+
200
+ attempt = 0
201
+ loop do
202
+ last = attempt >= @max_retries - 1
203
+ begin
204
+ res = @http.call(req)
205
+ rescue MisarDevError
206
+ raise
207
+ rescue StandardError => e
208
+ if !last && IDEMPOTENT.include?(verb)
209
+ sleep(self.class.backoff(attempt))
210
+ attempt += 1
211
+ next
212
+ end
213
+ raise NetworkError, e.message.to_s.empty? ? "Network error" : e.message
214
+ end
215
+
216
+ decoded = self.class.decode_body(res.status, res.body)
217
+ return decoded if res.status >= 200 && res.status < 300
218
+
219
+ # Read the body BEFORE deciding to retry: a 429 from the rate limiter
220
+ # and a 429 from a spent plan allowance share a status, and only the
221
+ # first is worth retrying.
222
+ error = self.class.error_for(res.status, res.headers, decoded, res.reason)
223
+ raise error if error.is_a?(PlanLimitError)
224
+
225
+ retryable = RETRYABLE.include?(res.status) && (res.status == 429 || IDEMPOTENT.include?(verb))
226
+ raise error unless retryable && !last
227
+
228
+ sleep(self.class.backoff(attempt, res.header("retry-after")))
229
+ attempt += 1
230
+ end
231
+ end
232
+
233
+ # Open a Server-Sent Events stream and yield each StreamEvent; returns an
234
+ # Enumerator when no block is given.
235
+ #
236
+ # Never retried: a stream that fails mid-flight cannot be replayed without
237
+ # duplicating what the caller already consumed. A
238
+ # {type:"error", code:"plan_limit_exceeded", upgrade} frame is raised as a
239
+ # PlanLimitError, exactly as the same refusal on a plain request would be.
240
+ def stream(method, path, query: nil, body: nil, terminator: "[DONE]", headers: {}, &block)
241
+ unless block
242
+ return enum_for(:stream, method, path, query: query, body: body, terminator: terminator, headers: headers)
243
+ end
244
+
245
+ req = Request.new(
246
+ method: method.to_s.upcase,
247
+ url: "#{@base_url}#{path}#{self.class.build_query(query)}",
248
+ headers: headers_for("text/event-stream", headers),
249
+ body: encode_body(body),
250
+ timeout: nil # a stream has no fixed duration
251
+ )
252
+
253
+ user_raised = false
254
+ deliver = lambda do |event, res|
255
+ # Other error frames are the stream's own business (the build stream
256
+ # reports a failed step and carries on), so only the plan refusal —
257
+ # which no amount of reading further will fix — is raised.
258
+ raise self.class.error_for(402, res.headers, event.data) if self.class.plan_limit_body?(event.data)
259
+
260
+ begin
261
+ block.call(event)
262
+ rescue Exception # rubocop:disable Lint/RescueException
263
+ user_raised = true
264
+ raise
265
+ end
266
+ end
267
+
268
+ begin
269
+ catch(:misardev_sse_done) do
270
+ @http.stream(req) do |res|
271
+ unless res.status >= 200 && res.status < 300
272
+ decoded = self.class.decode_body(res.status, res.read_all)
273
+ raise self.class.error_for(res.status, res.headers, decoded, res.reason)
274
+ end
275
+
276
+ parser = SSE.new(terminator)
277
+ res.each_chunk do |chunk|
278
+ parser.feed(chunk) { |event| deliver.call(event, res) }
279
+ # Leave the connection rather than reading on past the terminator.
280
+ throw :misardev_sse_done if parser.done?
281
+ end
282
+ parser.finish { |event| deliver.call(event, res) }
283
+ end
284
+ end
285
+ rescue MisarDevError
286
+ raise
287
+ rescue StandardError => e
288
+ raise if user_raised
289
+
290
+ raise NetworkError, e.message.to_s.empty? ? "Network error" : e.message
291
+ end
292
+ nil
293
+ end
294
+
295
+ private
296
+
297
+ def present(value)
298
+ value.nil? || value.to_s.empty? ? nil : value.to_s
299
+ end
300
+
301
+ def encode_body(body)
302
+ body.nil? ? nil : JSON.generate(body)
303
+ end
304
+
305
+ def headers_for(accept, extra)
306
+ {
307
+ "Authorization" => "Bearer #{@api_key}",
308
+ "Accept" => accept,
309
+ "Content-Type" => "application/json",
310
+ "X-Misar-SDK" => "misardev-ruby/#{VERSION}"
311
+ }.merge(@headers).merge(extra || {})
312
+ end
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MisarDev
4
+ # Base class for every error the SDK raises.
5
+ #
6
+ # Every non-2xx answer becomes a MisarDevError; the subclasses let a caller
7
+ # branch on the kind of failure without parsing strings.
8
+ # `rescue MisarDev::MisarDevError` (or its alias `MisarDev::Error`) catches
9
+ # everything the SDK raises.
10
+ class MisarDevError < StandardError
11
+ # @return [Integer] HTTP status, or 0 when the request never got an answer
12
+ attr_reader :status
13
+ # @return [String, nil] machine-readable error code from the body
14
+ attr_reader :code
15
+ # @return [Object, nil] the decoded error body
16
+ attr_reader :body
17
+ # @return [String, nil] the x-request-id header, for support requests
18
+ attr_reader :request_id
19
+
20
+ def initialize(status, message, code: nil, body: nil, request_id: nil)
21
+ @status = status
22
+ @code = code
23
+ @body = body
24
+ @request_id = request_id
25
+ super(message)
26
+ end
27
+
28
+ # Worth retrying as-is: a throttle or a transient server condition.
29
+ def retryable?
30
+ status == 429 || (status >= 500 && status < 600)
31
+ end
32
+ end
33
+
34
+ Error = MisarDevError
35
+
36
+ # 400 / 422 — the request was malformed or failed validation.
37
+ class ValidationError < MisarDevError; end
38
+
39
+ # 401 — the key is missing, revoked, expired, or not a MisarDev key.
40
+ class AuthenticationError < MisarDevError; end
41
+
42
+ # 403 — authenticated, but not allowed to touch this resource.
43
+ class PermissionDeniedError < MisarDevError; end
44
+
45
+ # 404 — no such resource, or not one this key can see.
46
+ class NotFoundError < MisarDevError; end
47
+
48
+ # 409 — conflicts with current state (e.g. a build already running).
49
+ class ConflictError < MisarDevError; end
50
+
51
+ # 5xx — the API failed. Retried automatically for idempotent requests.
52
+ class ServerError < MisarDevError; end
53
+
54
+ # 429 that is an ordinary throttle. Wait #retry_after seconds and try again —
55
+ # the transport already did so max_retries times before raising this.
56
+ class RateLimitError < MisarDevError
57
+ # @return [Float, nil] seconds the server asked us to wait, when it said
58
+ attr_reader :retry_after
59
+
60
+ def initialize(status, message, retry_after: nil, **kwargs)
61
+ @retry_after = retry_after
62
+ super(status, message, **kwargs)
63
+ end
64
+ end
65
+
66
+ # The request never got an HTTP answer (DNS, TLS, reset, timeout). #status is 0.
67
+ class NetworkError < MisarDevError
68
+ def initialize(message)
69
+ super(0, message, code: "network_error")
70
+ end
71
+ end
72
+
73
+ # The subscription behind the API key blocks this call.
74
+ #
75
+ # Detected by `code: "plan_limit_exceeded"` in the body — NEVER by status,
76
+ # because a spent quota is a 429 that would otherwise look like an ordinary
77
+ # throttle. A feature not on the plan answers 402 (`feature_locked`); a spent
78
+ # allowance answers 429 (`quota_exhausted`). Retrying cannot help until the
79
+ # plan changes or the period resets, so the transport never retries it.
80
+ class PlanLimitError < MisarDevError
81
+ # @return [Hash, nil] the full upgrade offer, every field the server sent.
82
+ # Render it; all copy comes from the server.
83
+ attr_reader :offer
84
+ # @return [Float, nil] seconds until a quota resets (quota_exhausted blocks)
85
+ attr_reader :retry_after
86
+
87
+ def initialize(status, message, offer: nil, retry_after: nil, body: nil, request_id: nil)
88
+ @offer = offer
89
+ @retry_after = retry_after
90
+ super(status, message, code: "plan_limit_exceeded", body: body, request_id: request_id)
91
+ end
92
+
93
+ # Where to send the user to unblock themselves: billing, else pricing.
94
+ def upgrade_url
95
+ urls = offer.is_a?(Hash) ? offer["urls"] : nil
96
+ return nil unless urls.is_a?(Hash)
97
+
98
+ urls["billing"] || urls["pricing"]
99
+ end
100
+
101
+ # The machine key of the limit that was hit, e.g. "api_access".
102
+ def feature
103
+ offer.is_a?(Hash) ? offer["feature"] : nil
104
+ end
105
+
106
+ # The slug of the plan the key is on now, e.g. "starter".
107
+ def current_plan
108
+ plan = offer.is_a?(Hash) ? offer["current_plan"] : nil
109
+ plan.is_a?(Hash) ? plan["slug"] : nil
110
+ end
111
+
112
+ # The slug of the plan the server recommends, e.g. "pro".
113
+ def recommended_plan
114
+ offer.is_a?(Hash) ? offer["recommended_plan"] : nil
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,72 @@
1
+ # GENERATED by scripts/generate-sdk-resources.mjs from sdks/manifest.json.
2
+ # Do not edit by hand — change the route, run `pnpm sdk:sync`, and regenerate.
3
+ # frozen_string_literal: true
4
+
5
+ module MisarDev
6
+ module Generated
7
+ # The `account` resource — 5 operation(s).
8
+ class AccountResource
9
+ def initialize(transport)
10
+ @t = transport
11
+ end
12
+
13
+ def inspect
14
+ "#<#{self.class.name}>"
15
+ end
16
+
17
+ # `POST /account/api-keys`
18
+ #
19
+ # misardev:method account.createKey
20
+ #
21
+ # @param body [Hash] JSON request body
22
+ # @return [Hash, Array, String, nil] decoded JSON, a raw string, or nil
23
+ # @raise [MisarDev::MisarDevError]
24
+ def create_key(body = {})
25
+ @t.request("POST", "/account/api-keys", body: body)
26
+ end
27
+
28
+ # `DELETE /account/api-keys/:keyId`
29
+ #
30
+ # misardev:method account.deleteKey
31
+ #
32
+ # @param key_id [String] path parameter `keyId`
33
+ # @return [Hash, Array, String, nil] decoded JSON, a raw string, or nil
34
+ # @raise [MisarDev::MisarDevError]
35
+ def delete_key(key_id)
36
+ @t.request("DELETE", "/account/api-keys/#{Core::Transport.segment(key_id)}")
37
+ end
38
+
39
+ # `GET /account/export`
40
+ #
41
+ # misardev:method account.export
42
+ #
43
+ # @return [Hash, Array, String, nil] decoded JSON, a raw string, or nil
44
+ # @raise [MisarDev::MisarDevError]
45
+ def export
46
+ @t.request("GET", "/account/export")
47
+ end
48
+
49
+ # `GET /account/api-keys`
50
+ #
51
+ # misardev:method account.listKeys
52
+ #
53
+ # @return [Hash, Array, String, nil] decoded JSON, a raw string, or nil
54
+ # @raise [MisarDev::MisarDevError]
55
+ def list_keys
56
+ @t.request("GET", "/account/api-keys")
57
+ end
58
+
59
+ # `PATCH /account/api-keys/:keyId`
60
+ #
61
+ # misardev:method account.updateKey
62
+ #
63
+ # @param key_id [String] path parameter `keyId`
64
+ # @param body [Hash] JSON request body
65
+ # @return [Hash, Array, String, nil] decoded JSON, a raw string, or nil
66
+ # @raise [MisarDev::MisarDevError]
67
+ def update_key(key_id, body = {})
68
+ @t.request("PATCH", "/account/api-keys/#{Core::Transport.segment(key_id)}", body: body)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,51 @@
1
+ # GENERATED by scripts/generate-sdk-resources.mjs from sdks/manifest.json.
2
+ # Do not edit by hand — change the route, run `pnpm sdk:sync`, and regenerate.
3
+ # frozen_string_literal: true
4
+
5
+ module MisarDev
6
+ module Generated
7
+ # The `apiKeys` resource — 3 operation(s).
8
+ class ApiKeysResource
9
+ def initialize(transport)
10
+ @t = transport
11
+ end
12
+
13
+ def inspect
14
+ "#<#{self.class.name}>"
15
+ end
16
+
17
+ # `POST /api-keys`
18
+ #
19
+ # misardev:method apiKeys.create
20
+ #
21
+ # @param body [Hash] JSON request body
22
+ # @return [Hash, Array, String, nil] decoded JSON, a raw string, or nil
23
+ # @raise [MisarDev::MisarDevError]
24
+ def create(body = {})
25
+ @t.request("POST", "/api-keys", body: body)
26
+ end
27
+
28
+ # `DELETE /api-keys`
29
+ #
30
+ # misardev:method apiKeys.delete
31
+ #
32
+ # @param body [Hash] JSON request body
33
+ # @return [Hash, Array, String, nil] decoded JSON, a raw string, or nil
34
+ # @raise [MisarDev::MisarDevError]
35
+ def delete(body = {})
36
+ @t.request("DELETE", "/api-keys", body: body)
37
+ end
38
+
39
+ # `GET /api-keys`
40
+ #
41
+ # misardev:method apiKeys.list
42
+ #
43
+ # @param query [Hash] optional query parameters (limit, offset); nils are dropped
44
+ # @return [Hash, Array, String, nil] decoded JSON, a raw string, or nil
45
+ # @raise [MisarDev::MisarDevError]
46
+ def list(query = {})
47
+ @t.request("GET", "/api-keys", query: query)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,28 @@
1
+ # GENERATED by scripts/generate-sdk-resources.mjs from sdks/manifest.json.
2
+ # Do not edit by hand — change the route, run `pnpm sdk:sync`, and regenerate.
3
+ # frozen_string_literal: true
4
+
5
+ module MisarDev
6
+ module Generated
7
+ # The `audit` resource — 1 operation(s).
8
+ class AuditResource
9
+ def initialize(transport)
10
+ @t = transport
11
+ end
12
+
13
+ def inspect
14
+ "#<#{self.class.name}>"
15
+ end
16
+
17
+ # `GET /audit`
18
+ #
19
+ # misardev:method audit.list
20
+ #
21
+ # @return [Hash, Array, String, nil] decoded JSON, a raw string, or nil
22
+ # @raise [MisarDev::MisarDevError]
23
+ def list
24
+ @t.request("GET", "/audit")
25
+ end
26
+ end
27
+ end
28
+ end