linkio 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,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "uri"
5
+
6
+ module LinkIO
7
+ # Stateless helpers for fingerprinting, platform detection and deep link
8
+ # building. Mirrors the utilities from the Node.js LinkIO backend.
9
+ module Utils
10
+ # Characters left unescaped by JavaScript's encodeURIComponent.
11
+ UNRESERVED = /[^A-Za-z0-9\-_.!~*'()]/n
12
+
13
+ module_function
14
+
15
+ # Generate a fingerprint from IP + User-Agent for deferred deep linking.
16
+ # This allows matching users before and after app install.
17
+ #
18
+ # @param ip [String]
19
+ # @param user_agent [String]
20
+ # @return [String] 32-character hex digest
21
+ def generate_fingerprint(ip, user_agent)
22
+ Digest::SHA256.hexdigest("#{ip}|#{user_agent}")[0, 32]
23
+ end
24
+
25
+ # Generate an IP-only fingerprint for cross-browser/app matching.
26
+ # Less precise but works when User-Agent differs between browser and app.
27
+ #
28
+ # @param ip [String]
29
+ # @return [String] 32-character hex digest
30
+ def generate_ip_fingerprint(ip)
31
+ Digest::SHA256.hexdigest(ip.to_s)[0, 32]
32
+ end
33
+
34
+ # Extract the client IP from request data, honouring the X-Forwarded-For
35
+ # header set by proxies.
36
+ #
37
+ # @param forwarded_for [String, nil] value of the X-Forwarded-For header
38
+ # @param ip [String, nil] framework-resolved remote IP
39
+ # @param remote_address [String, nil] raw socket remote address
40
+ # @return [String] the resolved client IP, or "unknown"
41
+ def client_ip(forwarded_for: nil, ip: nil, remote_address: nil)
42
+ return forwarded_for.to_s.split(",").first.to_s.strip if forwarded_for && !forwarded_for.to_s.empty?
43
+
44
+ value = ip || remote_address
45
+ value.nil? || value.to_s.empty? ? "unknown" : value.to_s
46
+ end
47
+
48
+ # Detect the platform from a User-Agent string.
49
+ #
50
+ # @param user_agent [String, nil]
51
+ # @return [String]
52
+ def detect_platform(user_agent)
53
+ Platform.detect(user_agent)
54
+ end
55
+
56
+ # Parse the query parameters of a URL into a string-keyed hash.
57
+ #
58
+ # @param url [String]
59
+ # @return [Hash{String => String}]
60
+ def parse_query_params(url)
61
+ query = URI.parse(url).query
62
+ return {} if query.nil? || query.empty?
63
+
64
+ params = {}
65
+ URI.decode_www_form(query).each do |key, value|
66
+ params[key] = if params.key?(key)
67
+ Array(params[key]) << value
68
+ else
69
+ value
70
+ end
71
+ end
72
+ params
73
+ rescue URI::InvalidURIError, ArgumentError
74
+ {}
75
+ end
76
+
77
+ # Build a custom-scheme deep link URI.
78
+ #
79
+ # @param scheme [String] e.g. "rokart"
80
+ # @param path [String] e.g. "link"
81
+ # @param params [Hash]
82
+ # @return [String] e.g. "rokart://link?foo=bar"
83
+ def build_deep_link(scheme, path, params)
84
+ query = encode_query(params)
85
+ "#{scheme}://#{path}#{"?#{query}" unless query.empty?}"
86
+ end
87
+
88
+ # URL-encode a params hash into a query string, matching the Node backend's
89
+ # `encodeURIComponent` behaviour.
90
+ #
91
+ # @param params [Hash]
92
+ # @return [String]
93
+ def encode_query(params)
94
+ params.map do |key, value|
95
+ "#{escape_component(key)}=#{escape_component(value)}"
96
+ end.join("&")
97
+ end
98
+
99
+ # Percent-encode a single component the way JavaScript's
100
+ # `encodeURIComponent` does (spaces become "%20", not "+").
101
+ #
102
+ # @param value [#to_s]
103
+ # @return [String]
104
+ def escape_component(value)
105
+ value.to_s.b.gsub(UNRESERVED) { |byte| format("%%%02X", byte.ord) }
106
+ end
107
+
108
+ # Substitute `{name}` placeholders in a template with URL-encoded values
109
+ # from +params+ (string or symbol keys). Missing keys become empty strings.
110
+ # Used to build role-specific fallback URLs, e.g.
111
+ # "https://example.com/refer?code={code}&role={role}"
112
+ #
113
+ # @param template [String]
114
+ # @param params [Hash]
115
+ # @return [String]
116
+ def interpolate(template, params)
117
+ template.to_s.gsub(/\{(\w+)\}/) do
118
+ key = Regexp.last_match(1)
119
+ value = params[key]
120
+ value = params[key.to_sym] if value.nil? && params.respond_to?(:key?)
121
+ escape_component(value)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkIO
4
+ VERSION = "1.0.0"
5
+ end
data/lib/linkio.rb ADDED
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "linkio/version"
4
+ require "linkio/errors"
5
+ require "linkio/platform"
6
+ require "linkio/utils"
7
+ require "linkio/pending_link_data"
8
+ require "linkio/referral_data"
9
+ require "linkio/deep_link_data"
10
+ require "linkio/request"
11
+ require "linkio/result"
12
+ require "linkio/app_target"
13
+ require "linkio/configuration"
14
+ require "linkio/storage/base"
15
+ require "linkio/storage/in_memory_storage"
16
+ require "linkio/storage/redis_storage"
17
+ require "linkio/smart_redirect"
18
+ require "linkio/client"
19
+
20
+ # LinkIO is a self-hosted deep linking backend for mobile apps -
21
+ # an open source alternative to Branch.io.
22
+ #
23
+ # The module also exposes a global configuration/client convenience API used by
24
+ # the Rack middleware and Rails engine:
25
+ #
26
+ # LinkIO.configure do |config|
27
+ # config.domain = "example.com"
28
+ # config.ios_app_id = "123456789"
29
+ # # ...
30
+ # config.storage = LinkIO::Storage::InMemoryStorage.new
31
+ # end
32
+ #
33
+ # LinkIO.client # => memoized LinkIO::Client
34
+ module LinkIO
35
+ class << self
36
+ # @return [LinkIO::Configuration]
37
+ def configuration
38
+ @configuration ||= Configuration.new
39
+ end
40
+
41
+ attr_writer :configuration
42
+
43
+ # Configure the global LinkIO instance.
44
+ #
45
+ # @yieldparam config [LinkIO::Configuration]
46
+ # @return [LinkIO::Configuration]
47
+ def configure
48
+ yield(configuration) if block_given?
49
+ configuration
50
+ end
51
+
52
+ # A memoized client built from the global configuration.
53
+ # Reset with {reset!} after changing configuration.
54
+ #
55
+ # @return [LinkIO::Client]
56
+ def client
57
+ @client ||= Client.new(configuration)
58
+ end
59
+
60
+ # Reset global configuration and client. Primarily useful in tests.
61
+ #
62
+ # @return [void]
63
+ def reset!
64
+ @configuration = nil
65
+ @client = nil
66
+ end
67
+ end
68
+ end
69
+
70
+ require "linkio/rack/middleware"
71
+ require "linkio/engine" if defined?(Rails::Engine)
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: linkio
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nakul Sharma
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.12'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.12'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.60'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.60'
68
+ description: |
69
+ LinkIO is a self-hosted deep linking solution for mobile applications, an open
70
+ source alternative to Branch.io. It generates Apple App Site Association and
71
+ Android assetlinks.json files, performs smart app-or-store redirects, supports
72
+ fingerprint-based deferred deep linking, and tracks referrals. It ships a
73
+ framework-agnostic core, a mountable Rails engine, and Rack middleware.
74
+ email:
75
+ - nakul@tecorb.co
76
+ executables: []
77
+ extensions: []
78
+ extra_rdoc_files: []
79
+ files:
80
+ - CHANGELOG.md
81
+ - LICENSE
82
+ - README.md
83
+ - app/controllers/linkio/application_controller.rb
84
+ - app/controllers/linkio/deep_links_controller.rb
85
+ - app/controllers/linkio/pending_links_controller.rb
86
+ - app/controllers/linkio/referrals_controller.rb
87
+ - app/controllers/linkio/well_known_controller.rb
88
+ - config/routes.rb
89
+ - lib/generators/linkio/install/install_generator.rb
90
+ - lib/generators/linkio/install/templates/linkio.rb
91
+ - lib/linkio.rb
92
+ - lib/linkio/app_target.rb
93
+ - lib/linkio/client.rb
94
+ - lib/linkio/configuration.rb
95
+ - lib/linkio/deep_link_data.rb
96
+ - lib/linkio/engine.rb
97
+ - lib/linkio/errors.rb
98
+ - lib/linkio/pending_link_data.rb
99
+ - lib/linkio/platform.rb
100
+ - lib/linkio/rack/middleware.rb
101
+ - lib/linkio/referral_data.rb
102
+ - lib/linkio/request.rb
103
+ - lib/linkio/result.rb
104
+ - lib/linkio/smart_redirect.rb
105
+ - lib/linkio/storage/base.rb
106
+ - lib/linkio/storage/in_memory_storage.rb
107
+ - lib/linkio/storage/redis_storage.rb
108
+ - lib/linkio/utils.rb
109
+ - lib/linkio/version.rb
110
+ homepage: https://github.com/pt-nakul-sharma/linkio-rails
111
+ licenses:
112
+ - MIT
113
+ metadata:
114
+ source_code_uri: https://github.com/pt-nakul-sharma/linkio-rails
115
+ bug_tracker_uri: https://github.com/pt-nakul-sharma/linkio-rails/issues
116
+ changelog_uri: https://github.com/pt-nakul-sharma/linkio-rails/blob/main/CHANGELOG.md
117
+ rubygems_mfa_required: 'true'
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 3.0.0
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubygems_version: 4.0.10
133
+ specification_version: 4
134
+ summary: Self-hosted deep linking backend for mobile apps - open source alternative
135
+ to Branch.io
136
+ test_files: []