vortex-ruby-sdk 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.
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+ require 'yard'
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+ RuboCop::RakeTask.new(:rubocop)
10
+ YARD::Rake::YardocTask.new
11
+
12
+ desc 'Run all tests and linting'
13
+ task test: [:spec, :rubocop]
14
+
15
+ desc 'Run tests with coverage'
16
+ task :coverage do
17
+ ENV['COVERAGE'] = 'true'
18
+ Rake::Task['spec'].execute
19
+ end
20
+
21
+ desc 'Generate documentation'
22
+ task :docs do
23
+ Rake::Task['yard'].execute
24
+ end
25
+
26
+ desc 'Setup development environment'
27
+ task :setup do
28
+ sh 'bundle install'
29
+ puts 'Development environment ready!'
30
+ end
31
+
32
+ desc 'Run interactive console'
33
+ task :console do
34
+ require 'bundler/setup'
35
+ require 'vortex'
36
+ require 'irb'
37
+ IRB.start(__FILE__)
38
+ end
39
+
40
+ task default: :test
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Basic usage example for Vortex Ruby SDK
5
+ #
6
+ # This example demonstrates how to use the core functionality of the Vortex Ruby SDK,
7
+ # showing the same operations available in all other Vortex SDKs (Node.js, Python, Java, Go).
8
+
9
+ require 'bundler/setup'
10
+ require 'vortex'
11
+
12
+ # Initialize the client with your API key
13
+ API_KEY = ENV['VORTEX_API_KEY'] || 'your-api-key-here'
14
+ client = Vortex::Client.new(API_KEY)
15
+
16
+ # Example user data - same structure as other SDKs
17
+ user_data = {
18
+ user_id: 'user123',
19
+ identifiers: [
20
+ { type: 'email', value: 'user@example.com' }
21
+ ],
22
+ groups: [
23
+ { id: 'team1', type: 'team', name: 'Engineering Team' },
24
+ { id: 'dept1', type: 'department', name: 'Product Development' }
25
+ ],
26
+ role: 'admin'
27
+ }
28
+
29
+ begin
30
+ puts "=== Vortex Ruby SDK Example ==="
31
+ puts
32
+
33
+ # 1. Generate JWT
34
+ puts "1. Generating JWT for user..."
35
+ jwt = client.generate_jwt(**user_data)
36
+ puts "JWT generated: #{jwt[0..50]}..."
37
+ puts
38
+
39
+ # 2. Get invitations by target
40
+ puts "2. Getting invitations by email target..."
41
+ invitations = client.get_invitations_by_target('email', 'user@example.com')
42
+ puts "Found #{invitations.length} invitation(s)"
43
+ puts
44
+
45
+ # 3. Get invitations by group
46
+ puts "3. Getting invitations for team group..."
47
+ group_invitations = client.get_invitations_by_group('team', 'team1')
48
+ puts "Found #{group_invitations.length} group invitation(s)"
49
+ puts
50
+
51
+ # 4. Example of accepting invitations (if any exist)
52
+ if invitations.any?
53
+ puts "4. Accepting first invitation..."
54
+ invitation_ids = [invitations.first['id']]
55
+ target = { type: 'email', value: 'user@example.com' }
56
+
57
+ result = client.accept_invitations(invitation_ids, target)
58
+ puts "Invitation accepted: #{result['id']}"
59
+ else
60
+ puts "4. No invitations to accept"
61
+ end
62
+ puts
63
+
64
+ puts "=== All operations completed successfully! ==="
65
+
66
+ rescue Vortex::VortexError => e
67
+ puts "Vortex error: #{e.message}"
68
+ exit 1
69
+ rescue => e
70
+ puts "Unexpected error: #{e.message}"
71
+ exit 1
72
+ end
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Rails application example for Vortex Ruby SDK
5
+ #
6
+ # This example shows how to integrate the Vortex SDK with a Rails application,
7
+ # providing the same API routes as other SDK integrations (Express, Java Spring, Python Flask).
8
+
9
+ # Gemfile additions needed:
10
+ # gem 'rails', '~> 7.0'
11
+ # gem 'vortex-ruby-sdk'
12
+
13
+ require 'rails'
14
+ require 'action_controller/railtie'
15
+ require 'vortex/rails'
16
+
17
+ class VortexExampleApp < Rails::Application
18
+ config.api_only = true
19
+ config.eager_load = false
20
+ config.logger = Logger.new(STDOUT)
21
+ end
22
+
23
+ # Example Vortex controller with authentication
24
+ class VortexController < ActionController::Base
25
+ include Vortex::Rails::Controller
26
+
27
+ private
28
+
29
+ # Implement user authentication - return user data hash or nil
30
+ def authenticate_vortex_user
31
+ # Example: get user from session/JWT/etc.
32
+ user_id = session[:user_id] || request.headers['X-User-ID']
33
+ return nil unless user_id
34
+
35
+ # Return user data in the format expected by Vortex
36
+ {
37
+ user_id: user_id,
38
+ identifiers: [
39
+ { type: 'email', value: session[:user_email] || 'user@example.com' }
40
+ ],
41
+ groups: [
42
+ { id: 'team1', type: 'team', name: 'Engineering' }
43
+ ],
44
+ role: session[:user_role] || 'user'
45
+ }
46
+ end
47
+
48
+ # Implement authorization - return true/false
49
+ def authorize_vortex_operation(operation, user)
50
+ # Example: check user permissions
51
+ case operation
52
+ when 'JWT'
53
+ true # Everyone can generate JWT if authenticated
54
+ when 'GET_INVITATIONS', 'GET_INVITATION'
55
+ true # Everyone can view invitations
56
+ when 'ACCEPT_INVITATIONS'
57
+ true # Everyone can accept invitations
58
+ when 'REVOKE_INVITATION', 'DELETE_GROUP_INVITATIONS'
59
+ user[:role] == 'admin' # Only admins can delete
60
+ when 'GET_GROUP_INVITATIONS', 'REINVITE'
61
+ user[:role] == 'admin' # Only admins can manage groups
62
+ else
63
+ false
64
+ end
65
+ end
66
+
67
+ # Provide Vortex client instance
68
+ def vortex_client
69
+ @vortex_client ||= Vortex::Client.new(
70
+ ENV['VORTEX_API_KEY'] || 'your-api-key-here'
71
+ )
72
+ end
73
+ end
74
+
75
+ # Configure routes - these match exactly with other SDK routes
76
+ Rails.application.routes.draw do
77
+ scope '/api/vortex', controller: 'vortex' do
78
+ post 'jwt', action: 'generate_jwt'
79
+ get 'invitations', action: 'get_invitations_by_target'
80
+ get 'invitations/:invitation_id', action: 'get_invitation'
81
+ delete 'invitations/:invitation_id', action: 'revoke_invitation'
82
+ post 'invitations/accept', action: 'accept_invitations'
83
+ get 'invitations/by-group/:group_type/:group_id', action: 'get_invitations_by_group'
84
+ delete 'invitations/by-group/:group_type/:group_id', action: 'delete_invitations_by_group'
85
+ post 'invitations/:invitation_id/reinvite', action: 'reinvite'
86
+ end
87
+
88
+ # Health check
89
+ get '/health', to: proc { [200, {}, ['OK']] }
90
+
91
+ # Root route with API documentation
92
+ root to: proc do
93
+ [200, { 'Content-Type' => 'application/json' }, [
94
+ {
95
+ name: 'Vortex Rails API',
96
+ version: Vortex::VERSION,
97
+ endpoints: {
98
+ jwt: 'POST /api/vortex/jwt',
99
+ invitations: 'GET /api/vortex/invitations?targetType=email&targetValue=user@example.com',
100
+ invitation: 'GET /api/vortex/invitations/:id',
101
+ revoke: 'DELETE /api/vortex/invitations/:id',
102
+ accept: 'POST /api/vortex/invitations/accept',
103
+ group_invitations: 'GET /api/vortex/invitations/by-group/:type/:id',
104
+ delete_group: 'DELETE /api/vortex/invitations/by-group/:type/:id',
105
+ reinvite: 'POST /api/vortex/invitations/:id/reinvite'
106
+ }
107
+ }.to_json
108
+ ]]
109
+ end
110
+ end
111
+
112
+ if __FILE__ == $0
113
+ puts "🚀 Starting Vortex Rails API server..."
114
+ puts "📊 Health check: http://localhost:3000/health"
115
+ puts "🔧 Vortex API routes available at http://localhost:3000/api/vortex"
116
+ puts
117
+ puts "Available endpoints:"
118
+ puts " POST /api/vortex/jwt"
119
+ puts " GET /api/vortex/invitations?targetType=email&targetValue=user@example.com"
120
+ puts " GET /api/vortex/invitations/:id"
121
+ puts " DELETE /api/vortex/invitations/:id"
122
+ puts " POST /api/vortex/invitations/accept"
123
+ puts " GET /api/vortex/invitations/by-group/:type/:id"
124
+ puts " DELETE /api/vortex/invitations/by-group/:type/:id"
125
+ puts " POST /api/vortex/invitations/:id/reinvite"
126
+
127
+ Rails.application.initialize!
128
+ Rails.application.run(Port: 3000)
129
+ end
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Sinatra application example for Vortex Ruby SDK
5
+ #
6
+ # This example shows how to integrate the Vortex SDK with a Sinatra application,
7
+ # providing the same API routes as other SDK integrations (Express, Java Spring, Python Flask).
8
+
9
+ require 'bundler/setup'
10
+ require 'sinatra/base'
11
+ require 'json'
12
+ require 'vortex/sinatra'
13
+
14
+ class VortexSinatraApp < Sinatra::Base
15
+ register Vortex::Sinatra
16
+
17
+ configure do
18
+ set :vortex_api_key, ENV['VORTEX_API_KEY'] || 'your-api-key-here'
19
+ set :vortex_base_url, ENV['VORTEX_BASE_URL'] # optional
20
+ end
21
+
22
+ # Implement user authentication
23
+ def authenticate_vortex_user
24
+ # Example: get user from headers/session/JWT
25
+ user_id = request.env['HTTP_X_USER_ID'] || 'demo-user'
26
+ return nil unless user_id
27
+
28
+ # Return user data in the format expected by Vortex
29
+ {
30
+ user_id: user_id,
31
+ identifiers: [
32
+ { type: 'email', value: request.env['HTTP_X_USER_EMAIL'] || 'demo@example.com' }
33
+ ],
34
+ groups: [
35
+ { id: 'team1', type: 'team', name: 'Engineering' }
36
+ ],
37
+ role: request.env['HTTP_X_USER_ROLE'] || 'user'
38
+ }
39
+ end
40
+
41
+ # Implement authorization
42
+ def authorize_vortex_operation(operation, user)
43
+ case operation
44
+ when 'JWT'
45
+ true # Everyone can generate JWT if authenticated
46
+ when 'GET_INVITATIONS', 'GET_INVITATION'
47
+ true # Everyone can view invitations
48
+ when 'ACCEPT_INVITATIONS'
49
+ true # Everyone can accept invitations
50
+ when 'REVOKE_INVITATION', 'DELETE_GROUP_INVITATIONS'
51
+ user[:role] == 'admin' # Only admins can delete
52
+ when 'GET_GROUP_INVITATIONS', 'REINVITE'
53
+ user[:role] == 'admin' # Only admins can manage groups
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ # Health check endpoint
60
+ get '/health' do
61
+ content_type :json
62
+ { status: 'OK', version: Vortex::VERSION }.to_json
63
+ end
64
+
65
+ # Root endpoint with API documentation
66
+ get '/' do
67
+ content_type :json
68
+ {
69
+ name: 'Vortex Sinatra API',
70
+ version: Vortex::VERSION,
71
+ endpoints: {
72
+ jwt: 'POST /api/vortex/jwt',
73
+ invitations: 'GET /api/vortex/invitations?targetType=email&targetValue=user@example.com',
74
+ invitation: 'GET /api/vortex/invitations/:id',
75
+ revoke: 'DELETE /api/vortex/invitations/:id',
76
+ accept: 'POST /api/vortex/invitations/accept',
77
+ group_invitations: 'GET /api/vortex/invitations/by-group/:type/:id',
78
+ delete_group: 'DELETE /api/vortex/invitations/by-group/:type/:id',
79
+ reinvite: 'POST /api/vortex/invitations/:id/reinvite'
80
+ }
81
+ }.to_json
82
+ end
83
+
84
+ # Error handlers
85
+ error Vortex::VortexError do
86
+ content_type :json
87
+ status 500
88
+ { error: env['sinatra.error'].message }.to_json
89
+ end
90
+
91
+ error do
92
+ content_type :json
93
+ status 500
94
+ { error: 'Internal server error' }.to_json
95
+ end
96
+ end
97
+
98
+ if __FILE__ == $0
99
+ puts "🚀 Starting Vortex Sinatra API server..."
100
+ puts "📊 Health check: http://localhost:4567/health"
101
+ puts "🔧 Vortex API routes available at http://localhost:4567/api/vortex"
102
+ puts
103
+ puts "Available endpoints:"
104
+ puts " POST /api/vortex/jwt"
105
+ puts " GET /api/vortex/invitations?targetType=email&targetValue=user@example.com"
106
+ puts " GET /api/vortex/invitations/:id"
107
+ puts " DELETE /api/vortex/invitations/:id"
108
+ puts " POST /api/vortex/invitations/accept"
109
+ puts " GET /api/vortex/invitations/by-group/:type/:id"
110
+ puts " DELETE /api/vortex/invitations/by-group/:type/:id"
111
+ puts " POST /api/vortex/invitations/:id/reinvite"
112
+ puts
113
+ puts "Authentication headers (for testing):"
114
+ puts " X-User-ID: your-user-id"
115
+ puts " X-User-Email: your-email@example.com"
116
+ puts " X-User-Role: user|admin"
117
+
118
+ VortexSinatraApp.run!
119
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'json'
5
+ require 'openssl'
6
+ require 'securerandom'
7
+ require 'faraday'
8
+
9
+ module Vortex
10
+ # Vortex API client for Ruby
11
+ #
12
+ # Provides the same functionality as other Vortex SDKs with JWT generation,
13
+ # invitation management, and full API compatibility.
14
+ class Client
15
+ # Base URL for Vortex API
16
+ DEFAULT_BASE_URL = 'https://api.vortexsoftware.com'
17
+
18
+ # @param api_key [String] Your Vortex API key
19
+ # @param base_url [String] Custom base URL (optional)
20
+ def initialize(api_key, base_url: nil)
21
+ @api_key = api_key
22
+ @base_url = base_url || DEFAULT_BASE_URL
23
+ @connection = build_connection
24
+ end
25
+
26
+ # Generate a JWT for the given user data
27
+ #
28
+ # This uses the exact same algorithm as the Node.js SDK to ensure
29
+ # complete compatibility across all platforms.
30
+ #
31
+ # @param user_id [String] Unique identifier for the user
32
+ # @param identifiers [Array<Hash>] Array of identifier hashes with :type and :value
33
+ # @param groups [Array<Hash>] Array of group hashes with :id, :type, and :name
34
+ # @param role [String, nil] Optional user role
35
+ # @return [String] JWT token
36
+ # @raise [VortexError] If API key is invalid or JWT generation fails
37
+ def generate_jwt(user_id:, identifiers:, groups:, role: nil)
38
+ # Parse API key - same format as Node.js SDK
39
+ prefix, encoded_id, key = @api_key.split('.')
40
+
41
+ raise VortexError, 'Invalid API key format' unless prefix && encoded_id && key
42
+ raise VortexError, 'Invalid API key prefix' unless prefix == 'VRTX'
43
+
44
+ # Decode the ID from base64url (same as Node.js Buffer.from(encodedId, 'base64url'))
45
+ decoded_bytes = Base64.urlsafe_decode64(encoded_id)
46
+
47
+ # Convert to UUID string format (same as uuidStringify in Node.js)
48
+ id = format_uuid(decoded_bytes)
49
+
50
+ expires = Time.now.to_i + 3600
51
+
52
+ # Step 1: Derive signing key from API key + ID (same as Node.js)
53
+ signing_key = OpenSSL::HMAC.digest('sha256', key, id)
54
+
55
+ # Step 2: Build header + payload (same structure as Node.js)
56
+ header = {
57
+ iat: Time.now.to_i,
58
+ alg: 'HS256',
59
+ typ: 'JWT',
60
+ kid: id
61
+ }
62
+
63
+ payload = {
64
+ userId: user_id,
65
+ groups: groups,
66
+ role: role,
67
+ expires: expires,
68
+ identifiers: identifiers
69
+ }
70
+
71
+ # Step 3: Base64URL encode (same as Node.js)
72
+ header_b64 = base64url_encode(JSON.generate(header))
73
+ payload_b64 = base64url_encode(JSON.generate(payload))
74
+
75
+ # Step 4: Sign with HMAC-SHA256 (same as Node.js)
76
+ signature = OpenSSL::HMAC.digest('sha256', signing_key, "#{header_b64}.#{payload_b64}")
77
+ signature_b64 = base64url_encode(signature)
78
+
79
+ "#{header_b64}.#{payload_b64}.#{signature_b64}"
80
+ rescue => e
81
+ raise VortexError, "JWT generation failed: #{e.message}"
82
+ end
83
+
84
+ # Get invitations by target
85
+ #
86
+ # @param target_type [String] Type of target (email, sms)
87
+ # @param target_value [String] Value of target (email address, phone number)
88
+ # @return [Array<Hash>] List of invitations
89
+ # @raise [VortexError] If the request fails
90
+ def get_invitations_by_target(target_type, target_value)
91
+ response = @connection.get('/api/v1/invitations') do |req|
92
+ req.params['targetType'] = target_type
93
+ req.params['targetValue'] = target_value
94
+ end
95
+
96
+ handle_response(response)['invitations'] || []
97
+ rescue => e
98
+ raise VortexError, "Failed to get invitations by target: #{e.message}"
99
+ end
100
+
101
+ # Get a specific invitation by ID
102
+ #
103
+ # @param invitation_id [String] The invitation ID
104
+ # @return [Hash] The invitation data
105
+ # @raise [VortexError] If the request fails
106
+ def get_invitation(invitation_id)
107
+ response = @connection.get("/api/v1/invitations/#{invitation_id}")
108
+ handle_response(response)
109
+ rescue => e
110
+ raise VortexError, "Failed to get invitation: #{e.message}"
111
+ end
112
+
113
+ # Revoke (delete) an invitation
114
+ #
115
+ # @param invitation_id [String] The invitation ID to revoke
116
+ # @return [Hash] Success response
117
+ # @raise [VortexError] If the request fails
118
+ def revoke_invitation(invitation_id)
119
+ response = @connection.delete("/api/v1/invitations/#{invitation_id}")
120
+ handle_response(response)
121
+ rescue => e
122
+ raise VortexError, "Failed to revoke invitation: #{e.message}"
123
+ end
124
+
125
+ # Accept invitations
126
+ #
127
+ # @param invitation_ids [Array<String>] List of invitation IDs to accept
128
+ # @param target [Hash] Target hash with :type and :value
129
+ # @return [Hash] The accepted invitation result
130
+ # @raise [VortexError] If the request fails
131
+ def accept_invitations(invitation_ids, target)
132
+ body = {
133
+ invitationIds: invitation_ids,
134
+ target: target
135
+ }
136
+
137
+ response = @connection.post('/api/v1/invitations/accept') do |req|
138
+ req.headers['Content-Type'] = 'application/json'
139
+ req.body = JSON.generate(body)
140
+ end
141
+
142
+ handle_response(response)
143
+ rescue => e
144
+ raise VortexError, "Failed to accept invitations: #{e.message}"
145
+ end
146
+
147
+ # Get invitations by group
148
+ #
149
+ # @param group_type [String] The group type
150
+ # @param group_id [String] The group ID
151
+ # @return [Array<Hash>] List of invitations for the group
152
+ # @raise [VortexError] If the request fails
153
+ def get_invitations_by_group(group_type, group_id)
154
+ response = @connection.get("/api/v1/invitations/by-group/#{group_type}/#{group_id}")
155
+ result = handle_response(response)
156
+ result['invitations'] || []
157
+ rescue => e
158
+ raise VortexError, "Failed to get group invitations: #{e.message}"
159
+ end
160
+
161
+ # Delete invitations by group
162
+ #
163
+ # @param group_type [String] The group type
164
+ # @param group_id [String] The group ID
165
+ # @return [Hash] Success response
166
+ # @raise [VortexError] If the request fails
167
+ def delete_invitations_by_group(group_type, group_id)
168
+ response = @connection.delete("/api/v1/invitations/by-group/#{group_type}/#{group_id}")
169
+ handle_response(response)
170
+ rescue => e
171
+ raise VortexError, "Failed to delete group invitations: #{e.message}"
172
+ end
173
+
174
+ # Reinvite a user
175
+ #
176
+ # @param invitation_id [String] The invitation ID to reinvite
177
+ # @return [Hash] The reinvited invitation result
178
+ # @raise [VortexError] If the request fails
179
+ def reinvite(invitation_id)
180
+ response = @connection.post("/api/v1/invitations/#{invitation_id}/reinvite")
181
+ handle_response(response)
182
+ rescue => e
183
+ raise VortexError, "Failed to reinvite: #{e.message}"
184
+ end
185
+
186
+ private
187
+
188
+ def build_connection
189
+ Faraday.new(@base_url) do |conn|
190
+ conn.request :json
191
+ conn.response :json, content_type: /\bjson$/
192
+ conn.adapter Faraday.default_adapter
193
+
194
+ # Add API key header (same as Node.js SDK)
195
+ conn.headers['x-api-key'] = @api_key
196
+ conn.headers['User-Agent'] = "vortex-ruby-sdk/#{Vortex::VERSION}"
197
+ end
198
+ end
199
+
200
+ def handle_response(response)
201
+ case response.status
202
+ when 200..299
203
+ response.body || {}
204
+ when 400..499
205
+ error_msg = response.body.is_a?(Hash) ? response.body['error'] || response.body['message'] : 'Client error'
206
+ raise VortexError, "Client error (#{response.status}): #{error_msg}"
207
+ when 500..599
208
+ error_msg = response.body.is_a?(Hash) ? response.body['error'] || response.body['message'] : 'Server error'
209
+ raise VortexError, "Server error (#{response.status}): #{error_msg}"
210
+ else
211
+ raise VortexError, "Unexpected response (#{response.status}): #{response.body}"
212
+ end
213
+ end
214
+
215
+ # Base64URL encode (no padding, URL-safe)
216
+ def base64url_encode(data)
217
+ Base64.urlsafe_encode64(data).tr('=', '')
218
+ end
219
+
220
+ # Format binary UUID data as string (same as Node.js uuidStringify)
221
+ def format_uuid(bytes)
222
+ return nil unless bytes.length == 16
223
+
224
+ # Convert to hex and format as UUID
225
+ hex = bytes.unpack1('H*')
226
+ "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vortex
4
+ # Custom error class for Vortex SDK exceptions
5
+ #
6
+ # All Vortex-related errors inherit from this class, making it easy
7
+ # to catch and handle Vortex-specific exceptions.
8
+ class VortexError < StandardError
9
+ # @param message [String] Error message
10
+ # @param cause [Exception, nil] Original exception that caused this error
11
+ def initialize(message = nil, cause = nil)
12
+ super(message)
13
+ @cause = cause
14
+ end
15
+
16
+ # @return [Exception, nil] The original exception that caused this error
17
+ attr_reader :cause
18
+ end
19
+ end