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.
@@ -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.get_invitations_by_group('team', 'team1')
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
 
@@ -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-group/:group_type/:group_id', action: 'get_invitations_by_group'
83
- delete 'invitations/by-group/:group_type/:group_id', action: 'delete_invitations_by_group'
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-group/:type/:id',
103
- delete_group: 'DELETE /api/vortex/invitations/by-group/:type/:id',
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-group/:type/:id"
123
- puts " DELETE /api/vortex/invitations/by-group/:type/:id"
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!
@@ -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-group/:type/:id',
77
- delete_group: 'DELETE /api/vortex/invitations/by-group/:type/:id',
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-group/:type/:id"
109
- puts " DELETE /api/vortex/invitations/by-group/:type/:id"
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
- def generate_jwt(params)
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
- expires = Time.now.to_i + 3600
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 (convert snake_case to camelCase for JWT)
84
- if user[:user_name]
85
- payload[:userName] = user[:user_name]
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 userAvatarUrl if present (convert snake_case to camelCase for JWT)
89
- if user[:user_avatar_url]
90
- payload[:userAvatarUrl] = user[:user_avatar_url]
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: user.compact # Remove nil values
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 group_type [String] The group type
275
- # @param group_id [String] The group ID
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 get_invitations_by_group(group_type, group_id)
279
- response = @connection.get("/api/v1/invitations/by-group/#{group_type}/#{group_id}")
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 group_type [String] The group type
289
- # @param group_id [String] The group ID
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 delete_invitations_by_group(group_type, group_id)
293
- response = @connection.delete("/api/v1/invitations/by-group/#{group_type}/#{group_id}")
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: '...', group_id: '...', name: '...' }]
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', group_id: 'team-789', name: 'Engineering' }],
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
- userName: inviter[:user_name],
367
- userAvatarUrl: inviter[:user_avatar_url]
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 widget_id [String] The widget configuration ID
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
- # 'widget-123',
700
+ # 'component-123',
452
701
  # 'Acme Corporation'
453
702
  # )
454
- def configure_autojoin(scope, scope_type, domains, widget_id, scope_name = nil, metadata = nil)
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, 'widget_id is required' if widget_id.nil? || widget_id.empty?
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
- widgetId: widget_id
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-group/:group_type/:group_id
185
- def get_invitations_by_group
186
- Vortex::Rails.logger.debug("Vortex::Rails::Controller#get_invitations_by_group invoked")
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
- group_type = params[:group_type]
196
- group_id = params[:group_id]
195
+ scope_type = params[:scope_type]
196
+ scope = params[:scope]
197
197
 
198
- invitations = vortex_client.get_invitations_by_group(group_type, group_id)
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-group/:group_type/:group_id
206
- def delete_invitations_by_group
207
- Vortex::Rails.logger.debug("Vortex::Rails::Controller#delete_invitations_by_group invoked")
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
- group_type = params[:group_type]
217
- group_id = params[:group_id]
216
+ scope_type = params[:scope_type]
217
+ scope = params[:scope]
218
218
 
219
- vortex_client.delete_invitations_by_group(group_type, group_id)
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-group/:group_type/:group_id', action: 'get_invitations_by_group'
300
- delete 'invitations/by-group/:group_type/:group_id', action: 'delete_invitations_by_group'
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