dregs 0.1.0 → 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,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ require_relative "models"
6
+
7
+ module Dregs
8
+ # What the namespaces on {Client} have in common: escaping path segments, checking arguments
9
+ # before anything leaves the process, and paging.
10
+ #
11
+ # The namespaces themselves are thin. Each method names the endpoint and hands the response to a
12
+ # model; the transport, the retries, and the error mapping all live on {Client}.
13
+ #
14
+ # @api private
15
+ class Resource
16
+ # The two directions a list can be sorted in.
17
+ ORDERS = %w[asc desc].freeze
18
+
19
+ # @api private
20
+ # @param client [Client]
21
+ def initialize(client)
22
+ @client = client
23
+ end
24
+
25
+ private
26
+
27
+ # Escapes one path segment, refusing an empty one.
28
+ #
29
+ # Ids are the caller's own and routinely contain characters that need escaping, an email
30
+ # address being the common one. A space is sent as +%20+ rather than +, and a slash is escaped
31
+ # so it cannot change the route.
32
+ def segment(value, what)
33
+ raise ArgumentError, "#{what} is required." if value.nil? || value.to_s.empty?
34
+
35
+ URI.encode_www_form_component(value.to_s).gsub("+", "%20")
36
+ end
37
+
38
+ def identity_segment(identity_id)
39
+ segment(identity_id, "An identity id")
40
+ end
41
+
42
+ # Fetches one page of a list.
43
+ #
44
+ # @param path [String] the list endpoint
45
+ # @param model [Class] the model each item is parsed into
46
+ # @param filters [Hash] the list's own query parameters, nil for those the caller left out
47
+ def list_page(path, model, filters, page:, page_size:, sort:, order:)
48
+ params = list_params(filters, page_size: page_size, sort: sort, order: order)
49
+
50
+ fetch_page(path, model, params, page_number(page))
51
+ end
52
+
53
+ # Walks every page of a list, fetching each as the one before it runs out.
54
+ #
55
+ # The arguments are checked now rather than when the first page is fetched, so a bad one
56
+ # raises where the call was made.
57
+ def list_every(path, model, filters, page_size:, sort:, order:, &block)
58
+ params = list_params(filters, page_size: page_size, sort: sort, order: order)
59
+
60
+ items = Enumerator.new do |yielder|
61
+ number = 0
62
+
63
+ loop do
64
+ page = fetch_page(path, model, params, number)
65
+
66
+ page.each { |item| yielder << item }
67
+
68
+ break unless page.has_next?
69
+
70
+ number += 1
71
+ end
72
+ end
73
+
74
+ return items unless block
75
+
76
+ items.each(&block)
77
+
78
+ nil
79
+ end
80
+
81
+ def fetch_page(path, model, params, number)
82
+ Page.from_api(@client.request(:get, path, params: params.merge("pageNumber" => number)), model)
83
+ end
84
+
85
+ def list_params(filters, page_size:, sort:, order:)
86
+ filters.merge("pageSize" => page_size, "sort" => sort, "order" => order_param(order))
87
+ end
88
+
89
+ def page_number(page)
90
+ raise ArgumentError, "Pages are numbered from 0." unless page.is_a?(Integer) && page >= 0
91
+
92
+ page
93
+ end
94
+
95
+ def order_param(order)
96
+ return nil if order.nil?
97
+
98
+ raise ArgumentError, "order is :asc or :desc." unless ORDERS.include?(order.to_s.downcase)
99
+
100
+ order.to_s.upcase
101
+ end
102
+
103
+ def attributes_hash(attributes)
104
+ return {} if attributes.nil?
105
+
106
+ return attributes if attributes.is_a?(Hash)
107
+
108
+ raise ArgumentError, "attributes must be a hash, such as { name: \"Acme Inc\" }."
109
+ end
110
+
111
+ def parse_each(model, payload)
112
+ items = Parsing.array_or_empty(payload).grep(Hash)
113
+
114
+ items.map { |item| model.from_api(item) }
115
+ end
116
+ end
117
+ end
data/lib/dregs/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Dregs
4
4
  # The version of this gem, reported in the User-Agent of every request it makes.
5
- VERSION = "0.1.0"
5
+ VERSION = "1.0.0"
6
6
  end
data/lib/dregs.rb CHANGED
@@ -3,7 +3,12 @@
3
3
  require_relative "dregs/version"
4
4
  require_relative "dregs/errors"
5
5
  require_relative "dregs/models"
6
+ require_relative "dregs/resource"
6
7
  require_relative "dregs/identities"
8
+ require_relative "dregs/groups"
9
+ require_relative "dregs/events"
10
+ require_relative "dregs/devices"
11
+ require_relative "dregs/escalations"
7
12
  require_relative "dregs/client"
8
13
  require_relative "dregs/webhooks"
9
14
 
data/sig/dregs.rbs CHANGED
@@ -60,7 +60,9 @@ module Dregs
60
60
 
61
61
  module Errors
62
62
  STATUS_ERRORS: Hash[Integer, Class]
63
+ REASON_PHRASES: Hash[Integer, String]
63
64
 
65
+ def self.reason_phrase: (Integer status_code) -> String
64
66
  def self.for_status: (Integer status_code) -> Class
65
67
  end
66
68
 
@@ -78,6 +80,7 @@ module Dregs
78
80
  def self?.time: (untyped value) -> Time?
79
81
  def self?.hash_or_empty: (untyped value) -> Hash[untyped, untyped]
80
82
  def self?.array_or_empty: (untyped value) -> Array[untyped]
83
+ def self?.integer_or_zero: (untyped value) -> Integer
81
84
  end
82
85
 
83
86
  class TrackResult
@@ -213,12 +216,413 @@ module Dregs
213
216
  def self.from_api: (untyped payload) -> Analysis
214
217
  end
215
218
 
216
- class Identities
219
+ class Page[T]
220
+ include Enumerable[T]
221
+
222
+ attr_reader to_a: Array[T]
223
+ attr_reader page_number: Integer
224
+ attr_reader page_size: Integer
225
+ attr_reader total_pages: Integer
226
+ attr_reader total_elements: Integer
227
+ attr_reader raw: Hash[untyped, untyped]
228
+
229
+ def initialize: (?Hash[Symbol, untyped] attributes) -> void
230
+ def each: () { (T) -> void } -> self
231
+ | () -> Enumerator[T, Page[T]]
232
+ def size: () -> Integer
233
+ def length: () -> Integer
234
+ def empty?: () -> bool
235
+ def []: (Integer index) -> T?
236
+ def first?: () -> bool
237
+ def last?: () -> bool
238
+ def next?: () -> bool
239
+ def has_next?: () -> bool
240
+ def self.from_api: (untyped payload, untyped model) -> Page[untyped]
241
+ end
242
+
243
+ class Reference
244
+ attr_reader id: String?
245
+ attr_reader label: String?
246
+ attr_reader path: String?
247
+ attr_reader raw: Hash[untyped, untyped]
248
+
249
+ def initialize: (id: String?, ?label: String?, ?path: String?, ?raw: Hash[untyped, untyped]) -> void
250
+ def self.from_api: (untyped payload) -> Reference?
251
+ end
252
+
253
+ class GroupReference
254
+ attr_reader type: String?
255
+ attr_reader id: String?
256
+ attr_reader label: String?
257
+ attr_reader path: String?
258
+ attr_reader raw: Hash[untyped, untyped]
259
+
260
+ def initialize: (
261
+ type: String?,
262
+ id: String?,
263
+ ?label: String?,
264
+ ?path: String?,
265
+ ?raw: Hash[untyped, untyped]
266
+ ) -> void
267
+
268
+ def self.from_api: (untyped payload) -> GroupReference
269
+ end
270
+
271
+ class Group
272
+ attr_reader type: String?
273
+ attr_reader id: String?
274
+ attr_reader display_name: String?
275
+ attr_reader identity_count: Integer?
276
+ attr_reader created_at: Time?
277
+ attr_reader updated_at: Time?
278
+ attr_reader last_tracked_at: Time?
279
+ attr_reader data: Hash[untyped, untyped]
280
+ attr_reader raw: Hash[untyped, untyped]
281
+
282
+ def initialize: (Hash[Symbol, untyped] attributes) -> void
283
+ def self.from_api: (untyped payload) -> Group
284
+ end
285
+
286
+ class Event
287
+ attr_reader id: String?
288
+ attr_reader timestamp: Time?
289
+ attr_reader identity: Reference?
290
+ attr_reader groups: Array[GroupReference]
291
+ attr_reader fingerprint: String?
292
+ attr_reader session: String?
293
+ attr_reader ip: String?
294
+ attr_reader url: String?
295
+ attr_reader source: String?
296
+ attr_reader name: String?
297
+ attr_reader canonical_type: String?
298
+ attr_reader data: Hash[untyped, untyped]
299
+ attr_reader raw: Hash[untyped, untyped]
300
+
301
+ def initialize: (Hash[Symbol, untyped] attributes) -> void
302
+ def self.from_api: (untyped payload) -> Event
303
+ end
304
+
305
+ class Device
306
+ attr_reader fingerprint: String?
307
+ attr_reader ip: String?
308
+ attr_reader asn: String?
309
+ attr_reader asn_label: String?
310
+ attr_reader user_agent: String?
311
+ attr_reader city: String?
312
+ attr_reader region: String?
313
+ attr_reader country: String?
314
+ attr_reader country_code: String?
315
+ attr_reader timezone: String?
316
+ attr_reader latitude: Float?
317
+ attr_reader longitude: Float?
318
+ attr_reader disregarded: bool
319
+ attr_reader first_seen_at: Time?
320
+ attr_reader last_seen_at: Time?
321
+ attr_reader raw: Hash[untyped, untyped]
322
+
323
+ def initialize: (Hash[Symbol, untyped] attributes) -> void
324
+ def disregarded?: () -> bool
325
+ def self.from_api: (untyped payload) -> Device
326
+ end
327
+
328
+ class LinkedIdentity
329
+ attr_reader identity_id: String?
330
+ attr_reader display_name: String?
331
+ attr_reader type: String?
332
+ attr_reader value: Float?
333
+ attr_reader confidence: Float?
334
+ attr_reader explanation: String?
335
+ attr_reader metadata: Hash[untyped, untyped]
336
+ attr_reader created_at: Time?
337
+ attr_reader raw: Hash[untyped, untyped]
338
+
339
+ def initialize: (Hash[Symbol, untyped] attributes) -> void
340
+ def self.from_api: (untyped payload) -> LinkedIdentity
341
+ end
342
+
343
+ class ScoreHistoryEntry
344
+ attr_reader cycle_id: Integer?
345
+ attr_reader humanity: Integer?
346
+ attr_reader authenticity: Integer?
347
+ attr_reader uniqueness: Integer?
348
+ attr_reader behavior: Integer?
349
+ attr_reader event_count: Integer?
350
+ attr_reader device_count: Integer?
351
+ attr_reader finished_at: Time?
352
+ attr_reader raw: Hash[untyped, untyped]
353
+
354
+ def initialize: (Hash[Symbol, untyped] attributes) -> void
355
+ def self.from_api: (untyped payload) -> ScoreHistoryEntry
356
+ end
357
+
358
+ class IdentityReview
359
+ attr_reader id: Integer?
360
+ attr_reader identity_id: String?
361
+ attr_reader status: String?
362
+ attr_reader feedback: String?
363
+ attr_reader summary: String?
364
+ attr_reader error_message: String?
365
+ attr_reader created_at: Time?
366
+ attr_reader completed_at: Time?
367
+ attr_reader raw: Hash[untyped, untyped]
368
+
369
+ def initialize: (Hash[Symbol, untyped] attributes) -> void
370
+ def self.from_api: (untyped payload) -> IdentityReview
371
+ end
372
+
373
+ class Escalation
374
+ attr_reader id: Integer?
375
+ attr_reader status: String?
376
+ attr_reader severity: String?
377
+ attr_reader identity: Reference?
378
+ attr_reader rule: Reference?
379
+ attr_reader scores: Hash[untyped, untyped]
380
+ attr_reader opened_at: Time?
381
+ attr_reader acknowledged_at: Time?
382
+ attr_reader closed_at: Time?
383
+ attr_reader created_at: Time?
384
+ attr_reader raw: Hash[untyped, untyped]
385
+
386
+ def initialize: (Hash[Symbol, untyped] attributes) -> void
387
+ def self.from_api: (untyped payload) -> Escalation
388
+ end
389
+
390
+ class EscalationSummary
391
+ attr_reader open: Integer
392
+ attr_reader acknowledged: Integer
393
+ attr_reader closed: Integer
394
+ attr_reader raw: Hash[untyped, untyped]
395
+
396
+ def initialize: (?open: Integer, ?acknowledged: Integer, ?closed: Integer, ?raw: Hash[untyped, untyped]) -> void
397
+ def self.from_api: (untyped payload) -> EscalationSummary
398
+ end
399
+
400
+ type order = Symbol | String
401
+
402
+ class Resource
403
+ ORDERS: Array[String]
404
+
217
405
  def initialize: (Client client) -> void
406
+
407
+ private
408
+
409
+ def segment: (untyped value, String what) -> String
410
+ def identity_segment: (untyped identity_id) -> String
411
+ def list_page: (
412
+ String path,
413
+ untyped model,
414
+ Hash[String, untyped] filters,
415
+ page: Integer,
416
+ page_size: Integer?,
417
+ sort: String?,
418
+ order: order?
419
+ ) -> Page[untyped]
420
+ def list_every: (
421
+ String path,
422
+ untyped model,
423
+ Hash[String, untyped] filters,
424
+ page_size: Integer?,
425
+ sort: String?,
426
+ order: order?
427
+ ) ?{ (untyped) -> void } -> Enumerator[untyped, void]?
428
+ def fetch_page: (String path, untyped model, Hash[String, untyped] params, Integer number) -> Page[untyped]
429
+ def list_params: (
430
+ Hash[String, untyped] filters,
431
+ page_size: Integer?,
432
+ sort: String?,
433
+ order: order?
434
+ ) -> Hash[String, untyped]
435
+ def page_number: (untyped page) -> Integer
436
+ def order_param: (order? order) -> String?
437
+ def attributes_hash: (untyped attributes) -> Hash[untyped, untyped]
438
+ def parse_each: (untyped model, untyped payload) -> Array[untyped]
439
+ end
440
+
441
+ class Identities < Resource
442
+ def list: (
443
+ ?term: String?,
444
+ ?group: String?,
445
+ ?group_type: String?,
446
+ ?device: String?,
447
+ ?badge: String?,
448
+ ?page: Integer,
449
+ ?page_size: Integer?,
450
+ ?sort: String?,
451
+ ?order: order?
452
+ ) -> Page[Identity]
453
+
454
+ def list_all: (
455
+ ?term: String?,
456
+ ?group: String?,
457
+ ?group_type: String?,
458
+ ?device: String?,
459
+ ?badge: String?,
460
+ ?page_size: Integer?,
461
+ ?sort: String?,
462
+ ?order: order?
463
+ ) { (Identity) -> void } -> nil
464
+ | (
465
+ ?term: String?,
466
+ ?group: String?,
467
+ ?group_type: String?,
468
+ ?device: String?,
469
+ ?badge: String?,
470
+ ?page_size: Integer?,
471
+ ?sort: String?,
472
+ ?order: order?
473
+ ) -> Enumerator[Identity, void]
474
+
218
475
  def get: (String identity_id) -> Identity
219
476
  def scores: (String identity_id) -> Scores
220
- def analysis: (String identity_id) -> Analysis
477
+ def analysis: (String identity_id, ?cycle_id: (Integer | String)?) -> Analysis
478
+ def history: (String identity_id, ?limit: Integer?) -> Array[ScoreHistoryEntry]
479
+ def links: (String identity_id) -> Array[LinkedIdentity]
221
480
  def analyze: (String identity_id) -> nil
481
+ def update: (String identity_id, ?disregarded: bool?, ?attributes: Hash[untyped, untyped]?) -> Identity
482
+ def reviews: (String identity_id) -> Array[IdentityReview]
483
+ def review: (String identity_id, ?feedback: String?) -> IdentityReview
484
+ end
485
+
486
+ class Groups < Resource
487
+ def list: (
488
+ ?group_type: String?,
489
+ ?identity: String?,
490
+ ?term: String?,
491
+ ?page: Integer,
492
+ ?page_size: Integer?,
493
+ ?sort: String?,
494
+ ?order: order?
495
+ ) -> Page[Group]
496
+
497
+ def list_all: (
498
+ ?group_type: String?,
499
+ ?identity: String?,
500
+ ?term: String?,
501
+ ?page_size: Integer?,
502
+ ?sort: String?,
503
+ ?order: order?
504
+ ) { (Group) -> void } -> nil
505
+ | (
506
+ ?group_type: String?,
507
+ ?identity: String?,
508
+ ?term: String?,
509
+ ?page_size: Integer?,
510
+ ?sort: String?,
511
+ ?order: order?
512
+ ) -> Enumerator[Group, void]
513
+
514
+ def get: (String group_type, String group_id) -> Group
515
+ def create: (String group_id, ?group_type: String?, ?attributes: Hash[untyped, untyped]?) -> Group
516
+ def update: (String group_type, String group_id, Hash[untyped, untyped]? attributes) -> Group
517
+ def add_member: (String group_type, String group_id, String identity_id) -> nil
518
+ def remove_member: (String group_type, String group_id, String identity_id) -> nil
519
+ end
520
+
521
+ class Events < Resource
522
+ def list: (
523
+ ?identity: String?,
524
+ ?group: String?,
525
+ ?group_type: String?,
526
+ ?device: String?,
527
+ ?session: String?,
528
+ ?term: String?,
529
+ ?page: Integer,
530
+ ?page_size: Integer?,
531
+ ?sort: String?,
532
+ ?order: order?
533
+ ) -> Page[Event]
534
+
535
+ def list_all: (
536
+ ?identity: String?,
537
+ ?group: String?,
538
+ ?group_type: String?,
539
+ ?device: String?,
540
+ ?session: String?,
541
+ ?term: String?,
542
+ ?page_size: Integer?,
543
+ ?sort: String?,
544
+ ?order: order?
545
+ ) { (Event) -> void } -> nil
546
+ | (
547
+ ?identity: String?,
548
+ ?group: String?,
549
+ ?group_type: String?,
550
+ ?device: String?,
551
+ ?session: String?,
552
+ ?term: String?,
553
+ ?page_size: Integer?,
554
+ ?sort: String?,
555
+ ?order: order?
556
+ ) -> Enumerator[Event, void]
557
+
558
+ def get: (String event_id) -> Event
559
+ end
560
+
561
+ class Devices < Resource
562
+ def list: (
563
+ ?identity: String?,
564
+ ?term: String?,
565
+ ?page: Integer,
566
+ ?page_size: Integer?,
567
+ ?sort: String?,
568
+ ?order: order?
569
+ ) -> Page[Device]
570
+
571
+ def list_all: (
572
+ ?identity: String?,
573
+ ?term: String?,
574
+ ?page_size: Integer?,
575
+ ?sort: String?,
576
+ ?order: order?
577
+ ) { (Device) -> void } -> nil
578
+ | (
579
+ ?identity: String?,
580
+ ?term: String?,
581
+ ?page_size: Integer?,
582
+ ?sort: String?,
583
+ ?order: order?
584
+ ) -> Enumerator[Device, void]
585
+
586
+ def get: (String fingerprint) -> Device
587
+ def update: (String fingerprint, disregarded: bool) -> Device
588
+ end
589
+
590
+ class Escalations < Resource
591
+ STATUSES: Array[String]
592
+
593
+ def list: (
594
+ ?status: (Symbol | String)?,
595
+ ?severity: (Symbol | String)?,
596
+ ?identity: String?,
597
+ ?rule: (Integer | String)?,
598
+ ?page: Integer,
599
+ ?page_size: Integer?,
600
+ ?sort: String?,
601
+ ?order: order?
602
+ ) -> Page[Escalation]
603
+
604
+ def list_all: (
605
+ ?status: (Symbol | String)?,
606
+ ?severity: (Symbol | String)?,
607
+ ?identity: String?,
608
+ ?rule: (Integer | String)?,
609
+ ?page_size: Integer?,
610
+ ?sort: String?,
611
+ ?order: order?
612
+ ) { (Escalation) -> void } -> nil
613
+ | (
614
+ ?status: (Symbol | String)?,
615
+ ?severity: (Symbol | String)?,
616
+ ?identity: String?,
617
+ ?rule: (Integer | String)?,
618
+ ?page_size: Integer?,
619
+ ?sort: String?,
620
+ ?order: order?
621
+ ) -> Enumerator[Escalation, void]
622
+
623
+ def get: (Integer | String escalation_id) -> Escalation
624
+ def update: (Integer | String escalation_id, status: Symbol | String) -> Escalation
625
+ def summary: () -> EscalationSummary
222
626
  end
223
627
 
224
628
  class Client
@@ -231,11 +635,16 @@ module Dregs
231
635
  MAX_BACKOFF_SECONDS: Float
232
636
  STATUS_RATE_LIMITED: String
233
637
  STATUS_QUOTA_EXCEEDED: String
638
+ DEFAULT_GROUP_TYPE: String
234
639
 
235
640
  attr_reader base_url: String
236
641
  attr_reader max_retries: Integer
237
642
  attr_reader timeout: Numeric
238
643
  attr_reader identities: Identities
644
+ attr_reader groups: Groups
645
+ attr_reader events: Events
646
+ attr_reader devices: Devices
647
+ attr_reader escalations: Escalations
239
648
 
240
649
  def initialize: (
241
650
  ?secret_key: String?,
@@ -249,12 +658,18 @@ module Dregs
249
658
  identity: String,
250
659
  ?data: Hash[untyped, untyped],
251
660
  ?identity_data: Hash[untyped, untyped],
661
+ ?groups: Array[Hash[untyped, untyped]]?,
252
662
  ?event_id: String?,
253
663
  ?timestamp: (Time | String)?,
254
664
  ?source: String?
255
665
  ) -> TrackResult
256
666
 
257
- def request: (Symbol method, String path, ?body: Hash[untyped, untyped]?) -> untyped
667
+ def request: (
668
+ Symbol method,
669
+ String path,
670
+ ?body: Hash[untyped, untyped]?,
671
+ ?params: Hash[String, untyped]?
672
+ ) -> untyped
258
673
  end
259
674
 
260
675
  module Webhooks
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dregs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dregs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-24 00:00:00.000000000 Z
11
+ date: 2026-10-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  The official Ruby client for Dregs, which scores the users of your application for fraud and
@@ -26,9 +26,14 @@ files:
26
26
  - SECURITY.md
27
27
  - lib/dregs.rb
28
28
  - lib/dregs/client.rb
29
+ - lib/dregs/devices.rb
29
30
  - lib/dregs/errors.rb
31
+ - lib/dregs/escalations.rb
32
+ - lib/dregs/events.rb
33
+ - lib/dregs/groups.rb
30
34
  - lib/dregs/identities.rb
31
35
  - lib/dregs/models.rb
36
+ - lib/dregs/resource.rb
32
37
  - lib/dregs/version.rb
33
38
  - lib/dregs/webhooks.rb
34
39
  - sig/dregs.rbs