smyte 0.1.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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e49d6787f785de76ce55f896fea08e2cfbc98ed2
4
+ data.tar.gz: 187e626f692fc20205f284fe398be27408d3f25e
5
+ SHA512:
6
+ metadata.gz: ecf2767e5f099f9ffc8e4a9f70f443bd9709c58347457230c6097a2ba5f7f37bbeedd2617d3d44786fd890622b099b5f3c003ba71d98ab5ecd5c55b07fef4102
7
+ data.tar.gz: 848e86930a5c87cdcaba635d86992524d75e08efc56775c734bfe7606f1e2619258ca6c1c3be5128af4147c44491908fb50eb8065e57ebda756d42e7b36883e2
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in smyte.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Brian Leonard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,77 @@
1
+ # Smyte
2
+
3
+ [Smyte](https://www.smyte.com) is a fraud-checking service. This gem makes it a bit easier to work with their [API](https://docs.smyte.com/).
4
+
5
+ ## Setup
6
+
7
+ `gem install smyte`
8
+
9
+ ```ruby
10
+ require 'smyte'
11
+
12
+ Smyte.enabled = true
13
+ Smyte.api_key = '123456789'
14
+ Smyte.api_secret = 'qwertyuiopasdfghjkl'
15
+ Smyte.webhook_secret = 'zxcvbnmpoiuytrewq'
16
+ ```
17
+
18
+ ## Sending data to Smyte
19
+
20
+ Uses this [API](https://docs.smyte.com/docs/sending-data-to-smyte).
21
+
22
+ ```ruby
23
+ payload = {
24
+ data: {
25
+ custom_id: 1
26
+ },
27
+ http_request: {
28
+ headers: {
29
+ "X-Real-IP" => "5.6.7.8"
30
+ },
31
+ path: "/login"
32
+ },
33
+ session: {
34
+ id: 'sessionguid',
35
+ action: {
36
+ user_id: 2
37
+ }
38
+ },
39
+ timestamp: Time.now
40
+ }
41
+
42
+ Smyte.action('my_event', payload)
43
+
44
+ ```
45
+
46
+ ## Receiving classification results
47
+
48
+ Uses this [API](https://docs.smyte.com/docs/receiving-classification-results).
49
+
50
+ ```ruby
51
+ # payload same as before (or other, of course)
52
+ classification = Smyte.classify('my_event', payload)
53
+
54
+ puts classification.action # will be :block, :review, or :allow
55
+ puts classification.label_report # for example on block ["exp:high_fail_rate_payment", "cc_country_mismatch"]
56
+
57
+ ```
58
+
59
+ ## Webhooks
60
+
61
+ Uses this [API](https://docs.smyte.com/docs/webhooks).
62
+ Smyte does a POST to your endpoint
63
+
64
+ ```ruby
65
+ # for example in a Rails controller action
66
+ secret = request.headers["X-Smyte-Signature"]
67
+ notification = ::Smyte.webhook(secret, params)
68
+ notification.items.each do |item|
69
+ puts item.action # will be :block, :review, or :allow
70
+ puts item.type # for example "user"
71
+ puts item.id # user id
72
+ puts item.label_report # for example on block ["bad_user"]
73
+ end
74
+
75
+ ```
76
+
77
+
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,45 @@
1
+ require "smyte/version"
2
+ require "forwardable"
3
+
4
+ module Smyte
5
+
6
+ autoload :Classification, 'smyte/classification'
7
+ autoload :Client, 'smyte/client'
8
+ autoload :Config, 'smyte/config'
9
+ autoload :LabelReporter, 'smyte/label_reporter'
10
+ autoload :Notification, 'smyte/notification'
11
+
12
+ class << self
13
+ extend Forwardable
14
+
15
+ def_delegators :config, :logger=, :logger,
16
+ :enabled=, :enabled, :enabled?,
17
+ :api_key=, :api_key,
18
+ :api_secret=, :api_secret,
19
+ :webhook_secret=, :webhook_secret
20
+
21
+ def_delegators :client, :action, :classify
22
+
23
+
24
+ def webhook(secret, response)
25
+ ::Smyte::Notification.parse(secret, response)
26
+ end
27
+
28
+ protected
29
+
30
+ def reset
31
+ # used by tests
32
+ @config = nil
33
+ end
34
+
35
+ def config
36
+ @config ||= ::Smyte::Config.new
37
+ end
38
+
39
+ def client
40
+ ::Smyte::Client.new
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,63 @@
1
+ module Smyte
2
+ class Classification
3
+ include ::Smyte::LabelReporter
4
+
5
+ def self.allowed
6
+ new('verdict' => "ALLOW", 'labels' => {})
7
+ end
8
+
9
+ attr_reader :response
10
+
11
+ def initialize(response)
12
+ @response = response
13
+ end
14
+
15
+ protected
16
+
17
+ def parse_labels
18
+ out = []
19
+ labels = response["labels"] || {}
20
+ labels.each do |name, attributes|
21
+ out << Smyte::Classification::Label.new(name, attributes)
22
+ end
23
+ out
24
+ end
25
+
26
+ def parse_allow?
27
+ response["verdict"] == "ALLOW"
28
+ end
29
+
30
+ class Label
31
+ attr_reader :name, :response
32
+ def initialize(name, response)
33
+ @name = name
34
+ @response = response
35
+ end
36
+
37
+ # returns :block, :review, :allow, :unknown
38
+ def action
39
+ @action ||= calculate_action
40
+ end
41
+
42
+ def experimental?
43
+ !response["enabled"]
44
+ end
45
+
46
+ protected
47
+
48
+ def calculate_action
49
+ return :allow if response["verdict"] == "ALLOW"
50
+
51
+ if response["verdict"] == "BLOCK"
52
+ if experimental?
53
+ return :review
54
+ else
55
+ return :block
56
+ end
57
+ end
58
+
59
+ return :unknown
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,80 @@
1
+ require 'faraday'
2
+ require 'json'
3
+ require 'logger'
4
+
5
+ module Smyte
6
+ class Client
7
+ attr_reader :api_key, :api_secret, :logger, :enabled
8
+
9
+ def initialize(options=nil)
10
+ options ||= {}
11
+ [:api_key, :api_secret, :logger, :enabled].each do |key|
12
+ if options.has_key?(key)
13
+ value = options[key]
14
+ else
15
+ value = Smyte.send(key)
16
+ end
17
+ instance_variable_set("@#{key}", value)
18
+ end
19
+ end
20
+
21
+ def action(event_name, payload)
22
+ return true if !enabled
23
+
24
+ process(event_name, payload, "/v2/action")
25
+ true # any success is just true - nothing else to know
26
+ end
27
+
28
+ def classify(event_name, payload)
29
+ return Smyte::Classification.allowed if !enabled
30
+
31
+ response = process(event_name, payload, "/v2/action/classify")
32
+ Smyte::Classification.new(response)
33
+ end
34
+
35
+ protected
36
+
37
+ def connection
38
+ return @connection if @connection
39
+ options = {
40
+ url: 'https://api.smyte.com',
41
+ headers: {
42
+ 'Content-Type' => 'application/json'
43
+ }
44
+ }
45
+ @connection = Faraday.new(options) do |faraday|
46
+ # faraday.response :logger # log requests to STDOUT
47
+ faraday.adapter Faraday.default_adapter
48
+ end
49
+ @connection.basic_auth(api_key, api_secret)
50
+ @connection
51
+ end
52
+
53
+ def process(event_name, payload, path)
54
+ payload ||= {}
55
+ if payload.has_key?('name')
56
+ payload['name'] = event_name
57
+ else
58
+ payload[:name] = event_name
59
+ end
60
+
61
+ response = connection.post do |req|
62
+ req.url path
63
+ req.body = payload.to_json
64
+ end
65
+
66
+ if response.success?
67
+ return JSON.parse(response.body)
68
+ else
69
+ hash = JSON.parse(response.body) rescue nil
70
+ error = "Smyte Error (#{response.status})"
71
+ if hash && hash["message"]
72
+ error << ": "
73
+ error << hash["message"]
74
+ error << "\n"
75
+ end
76
+ raise error
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,34 @@
1
+ module Smyte
2
+ class Config
3
+ attr_accessor :logger
4
+ attr_writer :api_key, :api_secret, :webhook_secret
5
+
6
+ def api_key
7
+ return @api_key if @api_key
8
+ raise "Smyte api_key not set"
9
+ end
10
+
11
+ def api_secret
12
+ return @api_secret if @api_secret
13
+ raise "Smyte api_secret not set"
14
+ end
15
+
16
+ def webhook_secret
17
+ return @webhook_secret if @webhook_secret
18
+ raise "Smyte webhook_secret not set"
19
+ end
20
+
21
+ def enabled=input
22
+ @enabled = !!(input.to_s =~ /^(t|1|y|ok)/)
23
+ end
24
+
25
+ def enabled
26
+ return true if @enabled.nil?
27
+ @enabled
28
+ end
29
+
30
+ def enabled?
31
+ enabled
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ module Smyte
2
+ module LabelReporter
3
+ # class needs to define parse_labels and parse_allow?
4
+ # which returns objects that have an action
5
+
6
+ # returns :block, :review, :allow, :unknown
7
+ def action
8
+ @action ||= calculate_action
9
+ end
10
+
11
+ def labels
12
+ @labels ||= parse_labels
13
+ end
14
+
15
+ def label_actions
16
+ return @label_actions if @label_actions
17
+ found = Hash.new { |hash, key| hash[key] = [] }
18
+ labels.each do |label|
19
+ found[label.action] << label
20
+ end
21
+
22
+ @label_actions = found
23
+ end
24
+
25
+ def label_report
26
+ return @label_report if @label_report
27
+ out = []
28
+
29
+ labels.each do |label|
30
+ case label.action
31
+ when :block, :review
32
+ out << label.name
33
+ end
34
+ end
35
+
36
+ @label_report = out
37
+ end
38
+
39
+ def reset_labels
40
+ remove_instance_variable(:@label_report) if defined?(@label_report)
41
+ remove_instance_variable(:@label_actions) if defined?(@label_actions)
42
+ remove_instance_variable(:@labels) if defined?(@labels)
43
+ remove_instance_variable(:@action) if defined?(@action)
44
+ end
45
+
46
+ protected
47
+
48
+ def calculate_action
49
+ return :allow if respond_to?(:parse_allow?, true) && parse_allow?
50
+
51
+ if label_actions[:block].size > 0
52
+ return :block
53
+ elsif label_actions[:review].size > 0
54
+ return :review
55
+ else
56
+ return :unknown
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,109 @@
1
+ module Smyte
2
+ class Notification
3
+ def self.parse(webhook_secret, response)
4
+ if webhook_secret != Smyte.webhook_secret
5
+ raise "invalid webhook_secret: #{webhook_secret}"
6
+ end
7
+ Smyte::Notification.new(response)
8
+ end
9
+
10
+ attr_reader :response
11
+
12
+ def initialize(response)
13
+ @response = response
14
+ end
15
+
16
+ def items
17
+ @items ||= parse_items
18
+ end
19
+
20
+ protected
21
+
22
+ def parse_items
23
+ out = {}
24
+ items = response["items"] || []
25
+ items.each do |hash|
26
+ key = hash["item"]
27
+ next unless key
28
+ if out[key]
29
+ out[key].send(:add_response, hash)
30
+ else
31
+ out[key] = Smyte::Notification::Item.new(hash)
32
+ end
33
+ end
34
+
35
+ out.values
36
+ end
37
+
38
+ class Item
39
+ include ::Smyte::LabelReporter
40
+
41
+ attr_reader :responses
42
+
43
+ def initialize(first_response)
44
+ @responses = [first_response]
45
+ end
46
+
47
+ def key
48
+ @item ||= responses.first["item"]
49
+ end
50
+
51
+ def type
52
+ @type ||= key.split("/").first
53
+ end
54
+
55
+ def id
56
+ @id ||= key.split("/").last
57
+ end
58
+
59
+ protected
60
+
61
+ def add_response(response)
62
+ reset_labels
63
+ responses << response
64
+ responses
65
+ end
66
+
67
+ def parse_labels
68
+ out = []
69
+ responses.each do |hash|
70
+ out << Smyte::Notification::Label.new(hash)
71
+ end
72
+ out
73
+ end
74
+ end
75
+
76
+ class Label
77
+ attr_reader :name, :response
78
+ def initialize(response)
79
+ @name = response["labelName"]
80
+ @response = response
81
+ end
82
+
83
+ # returns :block, :review, :allow, :unknown
84
+ def action
85
+ @action ||= calculate_action
86
+ end
87
+
88
+ def experimental?
89
+ response["labelType"] == "PENDING"
90
+ end
91
+
92
+ protected
93
+
94
+ def calculate_action
95
+ case response["labelType"]
96
+ when "ALLOW"
97
+ return :allow
98
+ when "PENDING"
99
+ return :review
100
+ when "ADDED", "BLOCK"
101
+ return :block
102
+ else
103
+ return :unknown
104
+ end
105
+ end
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,3 @@
1
+ module Smyte
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'smyte/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "smyte"
8
+ spec.version = Smyte::VERSION
9
+ spec.authors = ["Brian Leonard"]
10
+ spec.email = ["brian@bleonard.com"]
11
+
12
+ spec.summary = %q{Talk to Smyte service}
13
+ spec.description = %q{Smyte does fraud detection}
14
+ spec.homepage = "https://www.taskrabbit.com"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "faraday"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.10"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.4"
27
+ spec.add_development_dependency "byebug", "~> 3.5.1"
28
+ spec.add_development_dependency "vcr", "~> 3.0.1"
29
+ spec.add_development_dependency "webmock", "~> 1.24.3"
30
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smyte
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Brian Leonard
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-04-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 3.5.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 3.5.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: vcr
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 3.0.1
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 3.0.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 1.24.3
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 1.24.3
111
+ description: Smyte does fraud detection
112
+ email:
113
+ - brian@bleonard.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - Gemfile
121
+ - LICENSE.txt
122
+ - README.md
123
+ - Rakefile
124
+ - lib/smyte.rb
125
+ - lib/smyte/classification.rb
126
+ - lib/smyte/client.rb
127
+ - lib/smyte/config.rb
128
+ - lib/smyte/label_reporter.rb
129
+ - lib/smyte/notification.rb
130
+ - lib/smyte/version.rb
131
+ - smyte.gemspec
132
+ homepage: https://www.taskrabbit.com
133
+ licenses:
134
+ - MIT
135
+ metadata: {}
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 2.2.3
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: Talk to Smyte service
156
+ test_files: []
157
+ has_rdoc: