vortex-ruby-sdk 1.9.0 → 1.18.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.
- checksums.yaml +4 -4
- data/.claude/implementation-guide.md +55 -29
- data/README.md +679 -210
- data/examples/basic_usage.rb +1 -1
- data/examples/rails_app.rb +6 -6
- data/examples/sinatra_app.rb +4 -4
- data/lib/vortex/client.rb +324 -36
- data/lib/vortex/rails.rb +14 -14
- data/lib/vortex/sinatra.rb +10 -10
- data/lib/vortex/types.rb +11 -7
- data/lib/vortex/version.rb +1 -1
- metadata +2 -2
data/examples/basic_usage.rb
CHANGED
|
@@ -50,7 +50,7 @@ begin
|
|
|
50
50
|
|
|
51
51
|
# 3. Get invitations by group
|
|
52
52
|
puts "3. Getting invitations for team group..."
|
|
53
|
-
group_invitations = client.
|
|
53
|
+
group_invitations = client.get_invitations_by_scope('team', 'team1')
|
|
54
54
|
puts "Found #{group_invitations.length} group invitation(s)"
|
|
55
55
|
puts
|
|
56
56
|
|
data/examples/rails_app.rb
CHANGED
|
@@ -79,8 +79,8 @@ Rails.application.routes.draw do
|
|
|
79
79
|
get 'invitations/:invitation_id', action: 'get_invitation'
|
|
80
80
|
delete 'invitations/:invitation_id', action: 'revoke_invitation'
|
|
81
81
|
post 'invitations/accept', action: 'accept_invitations'
|
|
82
|
-
get 'invitations/by-
|
|
83
|
-
delete 'invitations/by-
|
|
82
|
+
get 'invitations/by-scope/:scope_type/:scope', action: 'get_invitations_by_scope'
|
|
83
|
+
delete 'invitations/by-scope/:scope_type/:scope', action: 'delete_invitations_by_scope'
|
|
84
84
|
post 'invitations/:invitation_id/reinvite', action: 'reinvite'
|
|
85
85
|
end
|
|
86
86
|
|
|
@@ -99,8 +99,8 @@ Rails.application.routes.draw do
|
|
|
99
99
|
invitation: 'GET /api/vortex/invitations/:id',
|
|
100
100
|
revoke: 'DELETE /api/vortex/invitations/:id',
|
|
101
101
|
accept: 'POST /api/vortex/invitations/accept',
|
|
102
|
-
group_invitations: 'GET /api/vortex/invitations/by-
|
|
103
|
-
delete_group: 'DELETE /api/vortex/invitations/by-
|
|
102
|
+
group_invitations: 'GET /api/vortex/invitations/by-scope/:type/:id',
|
|
103
|
+
delete_group: 'DELETE /api/vortex/invitations/by-scope/:type/:id',
|
|
104
104
|
reinvite: 'POST /api/vortex/invitations/:id/reinvite'
|
|
105
105
|
}
|
|
106
106
|
}.to_json
|
|
@@ -119,8 +119,8 @@ if __FILE__ == $0
|
|
|
119
119
|
puts " GET /api/vortex/invitations/:id"
|
|
120
120
|
puts " DELETE /api/vortex/invitations/:id"
|
|
121
121
|
puts " POST /api/vortex/invitations/accept"
|
|
122
|
-
puts " GET /api/vortex/invitations/by-
|
|
123
|
-
puts " DELETE /api/vortex/invitations/by-
|
|
122
|
+
puts " GET /api/vortex/invitations/by-scope/:type/:id"
|
|
123
|
+
puts " DELETE /api/vortex/invitations/by-scope/:type/:id"
|
|
124
124
|
puts " POST /api/vortex/invitations/:id/reinvite"
|
|
125
125
|
|
|
126
126
|
Rails.application.initialize!
|
data/examples/sinatra_app.rb
CHANGED
|
@@ -73,8 +73,8 @@ class VortexSinatraApp < Sinatra::Base
|
|
|
73
73
|
invitation: 'GET /api/vortex/invitations/:id',
|
|
74
74
|
revoke: 'DELETE /api/vortex/invitations/:id',
|
|
75
75
|
accept: 'POST /api/vortex/invitations/accept',
|
|
76
|
-
group_invitations: 'GET /api/vortex/invitations/by-
|
|
77
|
-
delete_group: 'DELETE /api/vortex/invitations/by-
|
|
76
|
+
group_invitations: 'GET /api/vortex/invitations/by-scope/:type/:id',
|
|
77
|
+
delete_group: 'DELETE /api/vortex/invitations/by-scope/:type/:id',
|
|
78
78
|
reinvite: 'POST /api/vortex/invitations/:id/reinvite'
|
|
79
79
|
}
|
|
80
80
|
}.to_json
|
|
@@ -105,8 +105,8 @@ if __FILE__ == $0
|
|
|
105
105
|
puts " GET /api/vortex/invitations/:id"
|
|
106
106
|
puts " DELETE /api/vortex/invitations/:id"
|
|
107
107
|
puts " POST /api/vortex/invitations/accept"
|
|
108
|
-
puts " GET /api/vortex/invitations/by-
|
|
109
|
-
puts " DELETE /api/vortex/invitations/by-
|
|
108
|
+
puts " GET /api/vortex/invitations/by-scope/:type/:id"
|
|
109
|
+
puts " DELETE /api/vortex/invitations/by-scope/:type/:id"
|
|
110
110
|
puts " POST /api/vortex/invitations/:id/reinvite"
|
|
111
111
|
puts
|
|
112
112
|
puts "Authentication headers (for testing):"
|
data/lib/vortex/client.rb
CHANGED
|
@@ -44,7 +44,85 @@ module Vortex
|
|
|
44
44
|
# user: { id: 'user-123', email: 'user@example.com' },
|
|
45
45
|
# attributes: { role: 'admin', department: 'Engineering' }
|
|
46
46
|
# })
|
|
47
|
-
|
|
47
|
+
# Sign a user object for use with the widget signature prop.
|
|
48
|
+
#
|
|
49
|
+
# @param user [Hash] User data with :id, :email, etc.
|
|
50
|
+
# @return [String] Signature in "kid:hexDigest" format
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# client = Vortex::Client.new(ENV['VORTEX_API_KEY'])
|
|
54
|
+
# signature = client.sign({ id: 'user-123', email: 'user@example.com' })
|
|
55
|
+
# # Pass signature to frontend alongside user prop
|
|
56
|
+
def sign(user)
|
|
57
|
+
user_id = user[:id] || user['id']
|
|
58
|
+
raise VortexError, 'User must have an :id field' if user_id.nil? || user_id.to_s.empty?
|
|
59
|
+
|
|
60
|
+
kid, signing_key = parse_and_derive_key
|
|
61
|
+
|
|
62
|
+
# Build canonical payload — include ALL user fields with key normalization
|
|
63
|
+
key_map = { id: 'userId', email: 'userEmail',
|
|
64
|
+
admin_scopes: 'adminScopes', allowed_email_domains: 'allowedEmailDomains' }
|
|
65
|
+
canonical = {}
|
|
66
|
+
user.each do |k, v|
|
|
67
|
+
str_key = k.to_s
|
|
68
|
+
mapped = key_map[k] || key_map[k.to_s.to_sym]
|
|
69
|
+
if mapped
|
|
70
|
+
canonical[mapped] = v
|
|
71
|
+
elsif str_key == 'id'
|
|
72
|
+
canonical['userId'] = v
|
|
73
|
+
elsif str_key == 'email'
|
|
74
|
+
canonical['userEmail'] = v
|
|
75
|
+
elsif !%w[name user_name avatar_url user_avatar_url].include?(str_key)
|
|
76
|
+
# Skip name/avatar fields here - handle them explicitly below
|
|
77
|
+
canonical[str_key] = v
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
canonical['userId'] = user_id # ensure normalized
|
|
81
|
+
|
|
82
|
+
# Prefer new property names (name/avatar_url), fall back to deprecated (user_name/user_avatar_url)
|
|
83
|
+
user_name = user[:name] || user[:user_name]
|
|
84
|
+
canonical['name'] = user_name if user_name
|
|
85
|
+
|
|
86
|
+
user_avatar_url = user[:avatar_url] || user[:user_avatar_url]
|
|
87
|
+
canonical['avatarUrl'] = user_avatar_url if user_avatar_url
|
|
88
|
+
|
|
89
|
+
# Recursive canonical JSON (sorted keys at every level)
|
|
90
|
+
canonical_json = JSON.generate(canonicalize_value(canonical))
|
|
91
|
+
|
|
92
|
+
digest = OpenSSL::HMAC.hexdigest('SHA256', signing_key, canonical_json)
|
|
93
|
+
"#{kid}:#{digest}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def canonicalize_value(value)
|
|
99
|
+
case value
|
|
100
|
+
when Hash
|
|
101
|
+
value.sort_by { |k, _| k.to_s }.map { |k, v| [k.to_s, canonicalize_value(v)] }.to_h
|
|
102
|
+
when Array
|
|
103
|
+
value.map { |item| canonicalize_value(item) }
|
|
104
|
+
else
|
|
105
|
+
value
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse_and_derive_key
|
|
110
|
+
parts = @api_key.split('.')
|
|
111
|
+
raise VortexError, 'Invalid API key format' unless parts.length == 3
|
|
112
|
+
prefix, encoded_id, key = parts
|
|
113
|
+
raise VortexError, 'Invalid API key format' unless prefix && encoded_id && key
|
|
114
|
+
raise VortexError, 'Invalid API key prefix' unless prefix == 'VRTX'
|
|
115
|
+
|
|
116
|
+
uuid_bytes = Base64.urlsafe_decode64(encoded_id)
|
|
117
|
+
raise VortexError, "Invalid API key: decoded UUID must be 16 bytes, got #{uuid_bytes.length}" unless uuid_bytes.length == 16
|
|
118
|
+
kid = format_uuid(uuid_bytes)
|
|
119
|
+
signing_key = OpenSSL::HMAC.digest('SHA256', key, kid)
|
|
120
|
+
[kid, signing_key]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
public
|
|
124
|
+
|
|
125
|
+
def generate_jwt(params, options = {})
|
|
48
126
|
user = params[:user]
|
|
49
127
|
attributes = params[:attributes]
|
|
50
128
|
|
|
@@ -60,7 +138,9 @@ module Vortex
|
|
|
60
138
|
# Convert to UUID string format (same as uuidStringify in Node.js)
|
|
61
139
|
id = format_uuid(decoded_bytes)
|
|
62
140
|
|
|
63
|
-
|
|
141
|
+
raw_expires = options[:expires_in] || options[:expiresIn]
|
|
142
|
+
expires_in_seconds = raw_expires ? parse_expires_in(raw_expires) : 2592000 # 30 days
|
|
143
|
+
expires = Time.now.to_i + expires_in_seconds
|
|
64
144
|
|
|
65
145
|
# Step 1: Derive signing key from API key + ID (same as Node.js)
|
|
66
146
|
signing_key = OpenSSL::HMAC.digest('sha256', key, id)
|
|
@@ -80,14 +160,16 @@ module Vortex
|
|
|
80
160
|
expires: expires
|
|
81
161
|
}
|
|
82
162
|
|
|
83
|
-
# Add name if present (
|
|
84
|
-
|
|
85
|
-
|
|
163
|
+
# Add name if present (prefer new property, fall back to deprecated)
|
|
164
|
+
user_name = user[:name] || user[:user_name]
|
|
165
|
+
if user_name
|
|
166
|
+
payload[:name] = user_name
|
|
86
167
|
end
|
|
87
168
|
|
|
88
|
-
# Add
|
|
89
|
-
|
|
90
|
-
|
|
169
|
+
# Add avatarUrl if present (prefer new property, fall back to deprecated)
|
|
170
|
+
user_avatar_url = user[:avatar_url] || user[:user_avatar_url]
|
|
171
|
+
if user_avatar_url
|
|
172
|
+
payload[:avatarUrl] = user_avatar_url
|
|
91
173
|
end
|
|
92
174
|
|
|
93
175
|
# Add adminScopes if present
|
|
@@ -118,6 +200,153 @@ module Vortex
|
|
|
118
200
|
raise VortexError, "JWT generation failed: #{e.message}"
|
|
119
201
|
end
|
|
120
202
|
|
|
203
|
+
# Generate a signed token for use with Vortex widgets.
|
|
204
|
+
#
|
|
205
|
+
# This method generates a signed JWT token containing your payload data.
|
|
206
|
+
# The token can be passed to widgets via the `token` prop to authenticate
|
|
207
|
+
# and authorize the request.
|
|
208
|
+
#
|
|
209
|
+
# @param payload [Hash] Data to sign (user, component, scope, vars, etc.)
|
|
210
|
+
# At minimum, include user[:id] for secure invitation attribution.
|
|
211
|
+
# @param options [Hash] Optional configuration
|
|
212
|
+
# @option options [String, Integer] :expires_in Expiration time (default: 30 days)
|
|
213
|
+
# Can be a duration string ("5m", "1h", "24h", "7d") or seconds as integer.
|
|
214
|
+
# @return [String] Signed JWT token
|
|
215
|
+
# @raise [VortexError] If API key format is invalid or token generation fails
|
|
216
|
+
#
|
|
217
|
+
# @example Sign just the user (minimum for secure attribution)
|
|
218
|
+
# token = client.generate_token({ user: { id: 'user-123' } })
|
|
219
|
+
#
|
|
220
|
+
# @example Sign full payload
|
|
221
|
+
# token = client.generate_token({
|
|
222
|
+
# component: 'widget-abc',
|
|
223
|
+
# user: { id: 'user-123', name: 'Peter', email: 'peter@example.com' },
|
|
224
|
+
# scope: 'workspace_456',
|
|
225
|
+
# vars: { company_name: 'Acme' }
|
|
226
|
+
# })
|
|
227
|
+
#
|
|
228
|
+
# @example Custom expiration (default is 30 days)
|
|
229
|
+
# token = client.generate_token(payload, { expires_in: '1h' })
|
|
230
|
+
# token = client.generate_token(payload, { expires_in: 3600 }) # seconds
|
|
231
|
+
def generate_token(payload, options = nil)
|
|
232
|
+
# Validate inputs
|
|
233
|
+
raise VortexError, "payload must be a Hash" unless payload.is_a?(Hash)
|
|
234
|
+
raise VortexError, "options must be a Hash or nil" if options && !options.is_a?(Hash)
|
|
235
|
+
|
|
236
|
+
# Normalize payload keys to symbols
|
|
237
|
+
payload = symbolize_keys(payload)
|
|
238
|
+
|
|
239
|
+
# Warn if user.id is missing
|
|
240
|
+
user = payload[:user]
|
|
241
|
+
if user.nil? || user[:id].nil? || user[:id].to_s.empty?
|
|
242
|
+
warn "[Vortex SDK] Warning: signing payload without user.id means invitations won't be securely attributed to a user."
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Parse expiration
|
|
246
|
+
expires_in_seconds = 30 * 24 * 60 * 60 # Default: 30 days
|
|
247
|
+
if options
|
|
248
|
+
options = symbolize_keys(options)
|
|
249
|
+
raw_expires = options[:expires_in] || options[:expiresIn]
|
|
250
|
+
expires_in_seconds = parse_expires_in(raw_expires) if raw_expires
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Parse API key and derive signing key
|
|
254
|
+
kid, signing_key = parse_and_derive_key
|
|
255
|
+
|
|
256
|
+
# Build JWT
|
|
257
|
+
now = Time.now.to_i
|
|
258
|
+
exp = now + expires_in_seconds
|
|
259
|
+
|
|
260
|
+
header = { alg: 'HS256', typ: 'JWT', kid: kid }
|
|
261
|
+
|
|
262
|
+
# Build JWT payload
|
|
263
|
+
jwt_payload = {}
|
|
264
|
+
|
|
265
|
+
# Add user if present
|
|
266
|
+
if user
|
|
267
|
+
user_map = {}
|
|
268
|
+
user_map[:id] = user[:id] if user[:id]
|
|
269
|
+
user_map[:email] = user[:email] if user[:email]
|
|
270
|
+
user_map[:name] = user[:name] if user[:name]
|
|
271
|
+
user_map[:avatarUrl] = user[:avatar_url] || user[:avatarUrl] if user[:avatar_url] || user[:avatarUrl]
|
|
272
|
+
user_map[:adminScopes] = user[:admin_scopes] || user[:adminScopes] if user[:admin_scopes] || user[:adminScopes]
|
|
273
|
+
user_map[:allowedEmailDomains] = user[:allowed_email_domains] || user[:allowedEmailDomains] if user[:allowed_email_domains] || user[:allowedEmailDomains]
|
|
274
|
+
jwt_payload[:user] = user_map unless user_map.empty?
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Add other payload fields
|
|
278
|
+
jwt_payload[:component] = payload[:component] if payload[:component]
|
|
279
|
+
jwt_payload[:scope] = payload[:scope] if payload[:scope]
|
|
280
|
+
jwt_payload[:vars] = payload[:vars] if payload[:vars] && !payload[:vars].empty?
|
|
281
|
+
|
|
282
|
+
# Add any extra fields from payload (except known keys)
|
|
283
|
+
known_keys = %i[user component scope vars]
|
|
284
|
+
payload.each do |k, v|
|
|
285
|
+
jwt_payload[k] = v unless known_keys.include?(k)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Add JWT claims
|
|
289
|
+
jwt_payload[:iat] = now
|
|
290
|
+
jwt_payload[:exp] = exp
|
|
291
|
+
|
|
292
|
+
# Encode header and payload
|
|
293
|
+
header_b64 = base64url_encode(JSON.generate(header))
|
|
294
|
+
payload_b64 = base64url_encode(JSON.generate(jwt_payload))
|
|
295
|
+
|
|
296
|
+
# Sign
|
|
297
|
+
to_sign = "#{header_b64}.#{payload_b64}"
|
|
298
|
+
signature = OpenSSL::HMAC.digest('SHA256', signing_key, to_sign)
|
|
299
|
+
signature_b64 = base64url_encode(signature)
|
|
300
|
+
|
|
301
|
+
"#{to_sign}.#{signature_b64}"
|
|
302
|
+
rescue => e
|
|
303
|
+
raise VortexError, "Token generation failed: #{e.message}"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
private
|
|
307
|
+
|
|
308
|
+
# Parse expiration time string or int into seconds.
|
|
309
|
+
# Supports: '5m', '1h', '24h', '7d' or raw seconds as int.
|
|
310
|
+
#
|
|
311
|
+
# @param expires_in [String, Integer] Expiration time
|
|
312
|
+
# @return [Integer] Expiration time in seconds
|
|
313
|
+
# @raise [VortexError] If format is invalid
|
|
314
|
+
def parse_expires_in(expires_in)
|
|
315
|
+
return 30 * 24 * 60 * 60 if expires_in.nil?
|
|
316
|
+
|
|
317
|
+
if expires_in.is_a?(Integer)
|
|
318
|
+
raise VortexError, "Invalid expires_in value: #{expires_in}. Numeric expires_in must be a positive integer number of seconds." if expires_in <= 0
|
|
319
|
+
return expires_in
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
unless expires_in.is_a?(String)
|
|
323
|
+
raise VortexError, "Invalid expires_in type: #{expires_in.class}. Must be String or Integer."
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
match = expires_in.match(/^(\d+)(m|h|d)$/)
|
|
327
|
+
unless match
|
|
328
|
+
raise VortexError, "Invalid expires_in format: \"#{expires_in}\". Use format like \"5m\", \"1h\", \"24h\", \"7d\" or a number of seconds."
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
value = match[1].to_i
|
|
332
|
+
if value <= 0
|
|
333
|
+
raise VortexError, "Invalid expires_in value: \"#{expires_in}\". Duration must be positive (e.g., \"5m\", \"1h\", \"7d\")."
|
|
334
|
+
end
|
|
335
|
+
unit = match[2]
|
|
336
|
+
|
|
337
|
+
multipliers = { 'm' => 60, 'h' => 60 * 60, 'd' => 60 * 60 * 24 }
|
|
338
|
+
value * multipliers[unit]
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Recursively convert hash keys to symbols
|
|
342
|
+
def symbolize_keys(hash)
|
|
343
|
+
return hash unless hash.is_a?(Hash)
|
|
344
|
+
hash.each_with_object({}) do |(k, v), result|
|
|
345
|
+
key = k.is_a?(String) ? k.to_sym : k
|
|
346
|
+
result[key] = v.is_a?(Hash) ? symbolize_keys(v) : v
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
121
350
|
public
|
|
122
351
|
|
|
123
352
|
# Get invitations by target
|
|
@@ -132,7 +361,7 @@ module Vortex
|
|
|
132
361
|
req.params['targetValue'] = target_value
|
|
133
362
|
end
|
|
134
363
|
|
|
135
|
-
handle_response(response)['invitations'] || []
|
|
364
|
+
transform_invitation_results(handle_response(response)['invitations'] || [])
|
|
136
365
|
rescue => e
|
|
137
366
|
raise VortexError, "Failed to get invitations by target: #{e.message}"
|
|
138
367
|
end
|
|
@@ -144,7 +373,7 @@ module Vortex
|
|
|
144
373
|
# @raise [VortexError] If the request fails
|
|
145
374
|
def get_invitation(invitation_id)
|
|
146
375
|
response = @connection.get("/api/v1/invitations/#{invitation_id}")
|
|
147
|
-
handle_response(response)
|
|
376
|
+
transform_invitation_result(handle_response(response))
|
|
148
377
|
rescue => e
|
|
149
378
|
raise VortexError, "Failed to get invitation: #{e.message}"
|
|
150
379
|
end
|
|
@@ -236,9 +465,17 @@ module Vortex
|
|
|
236
465
|
# Validate that either email or phone is provided
|
|
237
466
|
raise VortexError, 'User must have either email or phone' if user[:email].nil? && user[:phone].nil?
|
|
238
467
|
|
|
468
|
+
# Transform user keys to camelCase for API
|
|
469
|
+
api_user = user.compact.transform_keys do |key|
|
|
470
|
+
case key
|
|
471
|
+
when :is_existing then :isExisting
|
|
472
|
+
else key
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
239
476
|
body = {
|
|
240
477
|
invitationIds: invitation_ids,
|
|
241
|
-
user:
|
|
478
|
+
user: api_user
|
|
242
479
|
}
|
|
243
480
|
|
|
244
481
|
response = @connection.post('/api/v1/invitations/accept') do |req|
|
|
@@ -246,7 +483,7 @@ module Vortex
|
|
|
246
483
|
req.body = JSON.generate(body)
|
|
247
484
|
end
|
|
248
485
|
|
|
249
|
-
handle_response(response)
|
|
486
|
+
transform_invitation_result(handle_response(response))
|
|
250
487
|
rescue VortexError
|
|
251
488
|
raise
|
|
252
489
|
rescue => e
|
|
@@ -271,26 +508,26 @@ module Vortex
|
|
|
271
508
|
|
|
272
509
|
# Get invitations by group
|
|
273
510
|
#
|
|
274
|
-
# @param
|
|
275
|
-
# @param
|
|
511
|
+
# @param scope_type [String] The group type
|
|
512
|
+
# @param scope [String] The group ID
|
|
276
513
|
# @return [Array<Hash>] List of invitations for the group
|
|
277
514
|
# @raise [VortexError] If the request fails
|
|
278
|
-
def
|
|
279
|
-
response = @connection.get("/api/v1/invitations/by-
|
|
515
|
+
def get_invitations_by_scope(scope_type, scope)
|
|
516
|
+
response = @connection.get("/api/v1/invitations/by-scope/#{scope_type}/#{scope}")
|
|
280
517
|
result = handle_response(response)
|
|
281
|
-
result['invitations'] || []
|
|
518
|
+
transform_invitation_results(result['invitations'] || [])
|
|
282
519
|
rescue => e
|
|
283
520
|
raise VortexError, "Failed to get group invitations: #{e.message}"
|
|
284
521
|
end
|
|
285
522
|
|
|
286
523
|
# Delete invitations by group
|
|
287
524
|
#
|
|
288
|
-
# @param
|
|
289
|
-
# @param
|
|
525
|
+
# @param scope_type [String] The group type
|
|
526
|
+
# @param scope [String] The group ID
|
|
290
527
|
# @return [Hash] Success response
|
|
291
528
|
# @raise [VortexError] If the request fails
|
|
292
|
-
def
|
|
293
|
-
response = @connection.delete("/api/v1/invitations/by-
|
|
529
|
+
def delete_invitations_by_scope(scope_type, scope)
|
|
530
|
+
response = @connection.delete("/api/v1/invitations/by-scope/#{scope_type}/#{scope}")
|
|
294
531
|
handle_response(response)
|
|
295
532
|
rescue => e
|
|
296
533
|
raise VortexError, "Failed to delete group invitations: #{e.message}"
|
|
@@ -303,7 +540,7 @@ module Vortex
|
|
|
303
540
|
# @raise [VortexError] If the request fails
|
|
304
541
|
def reinvite(invitation_id)
|
|
305
542
|
response = @connection.post("/api/v1/invitations/#{invitation_id}/reinvite")
|
|
306
|
-
handle_response(response)
|
|
543
|
+
transform_invitation_result(handle_response(response))
|
|
307
544
|
rescue => e
|
|
308
545
|
raise VortexError, "Failed to reinvite: #{e.message}"
|
|
309
546
|
end
|
|
@@ -322,7 +559,7 @@ module Vortex
|
|
|
322
559
|
# @param widget_configuration_id [String] The widget configuration ID to use
|
|
323
560
|
# @param target [Hash] The invitation target: { type: 'email|sms|internal', value: '...' }
|
|
324
561
|
# @param inviter [Hash] The inviter info: { user_id: '...', user_email: '...', name: '...' }
|
|
325
|
-
# @param groups [Array<Hash>, nil] Optional groups: [{ type: '...',
|
|
562
|
+
# @param groups [Array<Hash>, nil] Optional groups: [{ type: '...', scope: '...', name: '...' }]
|
|
326
563
|
# @param source [String, nil] Optional source for analytics (defaults to 'api')
|
|
327
564
|
# @param subtype [String, nil] Optional subtype for analytics segmentation (e.g., 'pymk', 'find-friends')
|
|
328
565
|
# @param template_variables [Hash, nil] Optional template variables for email customization
|
|
@@ -336,7 +573,7 @@ module Vortex
|
|
|
336
573
|
# 'widget-config-123',
|
|
337
574
|
# { type: 'email', value: 'invitee@example.com' },
|
|
338
575
|
# { user_id: 'user-456', user_email: 'inviter@example.com', name: 'John Doe' },
|
|
339
|
-
# [{ type: 'team',
|
|
576
|
+
# [{ type: 'team', scope: 'team-789', name: 'Engineering' }],
|
|
340
577
|
# nil,
|
|
341
578
|
# nil,
|
|
342
579
|
# nil,
|
|
@@ -351,20 +588,28 @@ module Vortex
|
|
|
351
588
|
# nil,
|
|
352
589
|
# 'pymk'
|
|
353
590
|
# )
|
|
354
|
-
def create_invitation(widget_configuration_id, target, inviter, groups = nil, source = nil, subtype = nil, template_variables = nil, metadata = nil, unfurl_config = nil)
|
|
591
|
+
def create_invitation(widget_configuration_id, target, inviter, groups = nil, source = nil, subtype = nil, template_variables = nil, metadata = nil, unfurl_config = nil, scopes: nil, scope_id: nil, scope_type: nil, scope_name: nil)
|
|
355
592
|
raise VortexError, 'widget_configuration_id is required' if widget_configuration_id.nil? || widget_configuration_id.empty?
|
|
356
593
|
raise VortexError, 'target must have type and value' if target[:type].nil? || target[:value].nil?
|
|
357
594
|
raise VortexError, 'inviter must have user_id' if inviter[:user_id].nil?
|
|
358
595
|
|
|
596
|
+
# Scope translation: flat params > scopes > groups
|
|
597
|
+
if scope_id && groups.nil? && scopes.nil?
|
|
598
|
+
groups = [{ type: scope_type || '', scope_id: scope_id, name: scope_name || '' }]
|
|
599
|
+
elsif scopes && groups.nil?
|
|
600
|
+
groups = scopes
|
|
601
|
+
end
|
|
602
|
+
|
|
359
603
|
# Build request body with camelCase keys for the API
|
|
604
|
+
# Prefer new property names (name/avatar_url), fall back to deprecated (user_name/user_avatar_url)
|
|
360
605
|
body = {
|
|
361
606
|
widgetConfigurationId: widget_configuration_id,
|
|
362
607
|
target: target,
|
|
363
608
|
inviter: {
|
|
364
609
|
userId: inviter[:user_id],
|
|
365
610
|
userEmail: inviter[:user_email],
|
|
366
|
-
|
|
367
|
-
|
|
611
|
+
name: inviter[:name] || inviter[:user_name],
|
|
612
|
+
avatarUrl: inviter[:avatar_url] || inviter[:user_avatar_url]
|
|
368
613
|
}.compact
|
|
369
614
|
}
|
|
370
615
|
|
|
@@ -372,7 +617,7 @@ module Vortex
|
|
|
372
617
|
body[:groups] = groups.map do |g|
|
|
373
618
|
{
|
|
374
619
|
type: g[:type],
|
|
375
|
-
groupId: g[:group_id],
|
|
620
|
+
groupId: g[:scope] || g[:scope_id] || g[:group_id] || g[:scopeId] || g[:groupId],
|
|
376
621
|
name: g[:name]
|
|
377
622
|
}
|
|
378
623
|
end
|
|
@@ -421,7 +666,11 @@ module Vortex
|
|
|
421
666
|
encoded_scope = URI.encode_www_form_component(scope)
|
|
422
667
|
|
|
423
668
|
response = @connection.get("/api/v1/invitations/by-scope/#{encoded_scope_type}/#{encoded_scope}/autojoin")
|
|
424
|
-
handle_response(response)
|
|
669
|
+
result = handle_response(response)
|
|
670
|
+
if result.is_a?(Hash) && result['invitation'].is_a?(Hash)
|
|
671
|
+
result['invitation'] = transform_invitation_result(result['invitation'])
|
|
672
|
+
end
|
|
673
|
+
result
|
|
425
674
|
rescue VortexError
|
|
426
675
|
raise
|
|
427
676
|
rescue => e
|
|
@@ -437,7 +686,7 @@ module Vortex
|
|
|
437
686
|
# @param scope [String] The scope identifier (customer's group ID)
|
|
438
687
|
# @param scope_type [String] The type of scope (e.g., "organization", "team")
|
|
439
688
|
# @param domains [Array<String>] Array of domains to configure for autojoin
|
|
440
|
-
# @param
|
|
689
|
+
# @param component_id [String] The component ID
|
|
441
690
|
# @param scope_name [String, nil] Optional display name for the scope
|
|
442
691
|
# @param metadata [Hash, nil] Optional metadata to attach to the invitation
|
|
443
692
|
# @return [Hash] Response with :autojoin_domains array and :invitation
|
|
@@ -448,20 +697,20 @@ module Vortex
|
|
|
448
697
|
# 'acme-org',
|
|
449
698
|
# 'organization',
|
|
450
699
|
# ['acme.com', 'acme.org'],
|
|
451
|
-
# '
|
|
700
|
+
# 'component-123',
|
|
452
701
|
# 'Acme Corporation'
|
|
453
702
|
# )
|
|
454
|
-
def configure_autojoin(scope, scope_type, domains,
|
|
703
|
+
def configure_autojoin(scope, scope_type, domains, component_id, scope_name = nil, metadata = nil)
|
|
455
704
|
raise VortexError, 'scope is required' if scope.nil? || scope.empty?
|
|
456
705
|
raise VortexError, 'scope_type is required' if scope_type.nil? || scope_type.empty?
|
|
457
|
-
raise VortexError, '
|
|
706
|
+
raise VortexError, 'component_id is required' if component_id.nil? || component_id.empty?
|
|
458
707
|
raise VortexError, 'domains must be an array' unless domains.is_a?(Array)
|
|
459
708
|
|
|
460
709
|
body = {
|
|
461
710
|
scope: scope,
|
|
462
711
|
scopeType: scope_type,
|
|
463
712
|
domains: domains,
|
|
464
|
-
|
|
713
|
+
componentId: component_id
|
|
465
714
|
}
|
|
466
715
|
|
|
467
716
|
body[:scopeName] = scope_name if scope_name
|
|
@@ -472,7 +721,11 @@ module Vortex
|
|
|
472
721
|
req.body = JSON.generate(body)
|
|
473
722
|
end
|
|
474
723
|
|
|
475
|
-
handle_response(response)
|
|
724
|
+
result = handle_response(response)
|
|
725
|
+
if result.is_a?(Hash) && result['invitation'].is_a?(Hash)
|
|
726
|
+
result['invitation'] = transform_invitation_result(result['invitation'])
|
|
727
|
+
end
|
|
728
|
+
result
|
|
476
729
|
rescue VortexError
|
|
477
730
|
raise
|
|
478
731
|
rescue => e
|
|
@@ -521,6 +774,29 @@ module Vortex
|
|
|
521
774
|
|
|
522
775
|
private
|
|
523
776
|
|
|
777
|
+
# Transform an invitation scope by adding scope_id (= groupId)
|
|
778
|
+
def transform_scope(scope)
|
|
779
|
+
return scope unless scope.is_a?(Hash) && scope.key?('groupId')
|
|
780
|
+
scope.merge('scopeId' => scope['groupId'])
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
# Transform an invitation result by adding scopes (= groups with scopeId)
|
|
784
|
+
def transform_invitation_result(result)
|
|
785
|
+
return result unless result.is_a?(Hash)
|
|
786
|
+
if result['groups'].is_a?(Array)
|
|
787
|
+
mapped = result['groups'].map { |g| transform_scope(g) }
|
|
788
|
+
result['scopes'] = mapped
|
|
789
|
+
result['groups'] = mapped
|
|
790
|
+
end
|
|
791
|
+
result
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Transform an array of invitation results
|
|
795
|
+
def transform_invitation_results(results)
|
|
796
|
+
return results unless results.is_a?(Array)
|
|
797
|
+
results.map { |r| transform_invitation_result(r) }
|
|
798
|
+
end
|
|
799
|
+
|
|
524
800
|
def build_connection
|
|
525
801
|
Faraday.new(@base_url) do |conn|
|
|
526
802
|
conn.request :json
|
|
@@ -563,5 +839,17 @@ module Vortex
|
|
|
563
839
|
hex = bytes.unpack1('H*')
|
|
564
840
|
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
565
841
|
end
|
|
842
|
+
|
|
843
|
+
# Deprecated methods for backward compatibility
|
|
844
|
+
|
|
845
|
+
# @deprecated Use get_invitations_by_scope instead
|
|
846
|
+
def get_invitations_by_group(group_type, group)
|
|
847
|
+
get_invitations_by_scope(group_type, group)
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
# @deprecated Use delete_invitations_by_scope instead
|
|
851
|
+
def delete_invitations_by_group(group_type, group)
|
|
852
|
+
delete_invitations_by_scope(group_type, group)
|
|
853
|
+
end
|
|
566
854
|
end
|
|
567
|
-
end
|
|
855
|
+
end
|
data/lib/vortex/rails.rb
CHANGED
|
@@ -181,9 +181,9 @@ module Vortex
|
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
# Get invitations by group
|
|
184
|
-
# GET /api/vortex/invitations/by-
|
|
185
|
-
def
|
|
186
|
-
Vortex::Rails.logger.debug("Vortex::Rails::Controller#
|
|
184
|
+
# GET /api/vortex/invitations/by-scope/:scope_type/:scope
|
|
185
|
+
def get_invitations_by_scope
|
|
186
|
+
Vortex::Rails.logger.debug("Vortex::Rails::Controller#get_invitations_by_scope invoked")
|
|
187
187
|
|
|
188
188
|
user = authenticate_vortex_user
|
|
189
189
|
return render_unauthorized('Authentication required') unless user
|
|
@@ -192,19 +192,19 @@ module Vortex
|
|
|
192
192
|
return render_forbidden('Not authorized to get group invitations')
|
|
193
193
|
end
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
scope_type = params[:scope_type]
|
|
196
|
+
scope = params[:scope]
|
|
197
197
|
|
|
198
|
-
invitations = vortex_client.
|
|
198
|
+
invitations = vortex_client.get_invitations_by_scope(scope_type, scope)
|
|
199
199
|
render json: { invitations: invitations }
|
|
200
200
|
rescue Vortex::VortexError => e
|
|
201
201
|
render_server_error("Failed to get group invitations: #{e.message}")
|
|
202
202
|
end
|
|
203
203
|
|
|
204
204
|
# Delete invitations by group
|
|
205
|
-
# DELETE /api/vortex/invitations/by-
|
|
206
|
-
def
|
|
207
|
-
Vortex::Rails.logger.debug("Vortex::Rails::Controller#
|
|
205
|
+
# DELETE /api/vortex/invitations/by-scope/:scope_type/:scope
|
|
206
|
+
def delete_invitations_by_scope
|
|
207
|
+
Vortex::Rails.logger.debug("Vortex::Rails::Controller#delete_invitations_by_scope invoked")
|
|
208
208
|
|
|
209
209
|
user = authenticate_vortex_user
|
|
210
210
|
return render_unauthorized('Authentication required') unless user
|
|
@@ -213,10 +213,10 @@ module Vortex
|
|
|
213
213
|
return render_forbidden('Not authorized to delete group invitations')
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
scope_type = params[:scope_type]
|
|
217
|
+
scope = params[:scope]
|
|
218
218
|
|
|
219
|
-
vortex_client.
|
|
219
|
+
vortex_client.delete_invitations_by_scope(scope_type, scope)
|
|
220
220
|
render json: { success: true }
|
|
221
221
|
rescue Vortex::VortexError => e
|
|
222
222
|
render_server_error("Failed to delete group invitations: #{e.message}")
|
|
@@ -296,8 +296,8 @@ module Vortex
|
|
|
296
296
|
get 'invitations/:invitation_id', action: 'get_invitation'
|
|
297
297
|
delete 'invitations/:invitation_id', action: 'revoke_invitation'
|
|
298
298
|
post 'invitations/accept', action: 'accept_invitations'
|
|
299
|
-
get 'invitations/by-
|
|
300
|
-
delete 'invitations/by-
|
|
299
|
+
get 'invitations/by-scope/:scope_type/:scope', action: 'get_invitations_by_scope'
|
|
300
|
+
delete 'invitations/by-scope/:scope_type/:scope', action: 'delete_invitations_by_scope'
|
|
301
301
|
post 'invitations/:invitation_id/reinvite', action: 'reinvite'
|
|
302
302
|
end
|
|
303
303
|
end
|