vortex-ruby-sdk 1.8.4 → 1.15.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/lib/vortex/client.rb CHANGED
@@ -44,6 +44,84 @@ module Vortex
44
44
  # user: { id: 'user-123', email: 'user@example.com' },
45
45
  # attributes: { role: 'admin', department: 'Engineering' }
46
46
  # })
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
+
47
125
  def generate_jwt(params)
48
126
  user = params[:user]
49
127
  attributes = params[:attributes]
@@ -80,14 +158,16 @@ module Vortex
80
158
  expires: expires
81
159
  }
82
160
 
83
- # Add name if present (convert snake_case to camelCase for JWT)
84
- if user[:user_name]
85
- payload[:userName] = user[:user_name]
161
+ # Add name if present (prefer new property, fall back to deprecated)
162
+ user_name = user[:name] || user[:user_name]
163
+ if user_name
164
+ payload[:name] = user_name
86
165
  end
87
166
 
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]
167
+ # Add avatarUrl if present (prefer new property, fall back to deprecated)
168
+ user_avatar_url = user[:avatar_url] || user[:user_avatar_url]
169
+ if user_avatar_url
170
+ payload[:avatarUrl] = user_avatar_url
91
171
  end
92
172
 
93
173
  # Add adminScopes if present
@@ -118,6 +198,153 @@ module Vortex
118
198
  raise VortexError, "JWT generation failed: #{e.message}"
119
199
  end
120
200
 
201
+ # Generate a signed token for use with Vortex widgets.
202
+ #
203
+ # This method generates a signed JWT token containing your payload data.
204
+ # The token can be passed to widgets via the `token` prop to authenticate
205
+ # and authorize the request.
206
+ #
207
+ # @param payload [Hash] Data to sign (user, component, scope, vars, etc.)
208
+ # At minimum, include user[:id] for secure invitation attribution.
209
+ # @param options [Hash] Optional configuration
210
+ # @option options [String, Integer] :expires_in Expiration time (default: 5 minutes)
211
+ # Can be a duration string ("5m", "1h", "24h", "7d") or seconds as integer.
212
+ # @return [String] Signed JWT token
213
+ # @raise [VortexError] If API key format is invalid or token generation fails
214
+ #
215
+ # @example Sign just the user (minimum for secure attribution)
216
+ # token = client.generate_token({ user: { id: 'user-123' } })
217
+ #
218
+ # @example Sign full payload
219
+ # token = client.generate_token({
220
+ # component: 'widget-abc',
221
+ # user: { id: 'user-123', name: 'Peter', email: 'peter@example.com' },
222
+ # scope: 'workspace_456',
223
+ # vars: { company_name: 'Acme' }
224
+ # })
225
+ #
226
+ # @example Custom expiration (default is 5 minutes)
227
+ # token = client.generate_token(payload, { expires_in: '1h' })
228
+ # token = client.generate_token(payload, { expires_in: 3600 }) # seconds
229
+ def generate_token(payload, options = nil)
230
+ # Validate inputs
231
+ raise VortexError, "payload must be a Hash" unless payload.is_a?(Hash)
232
+ raise VortexError, "options must be a Hash or nil" if options && !options.is_a?(Hash)
233
+
234
+ # Normalize payload keys to symbols
235
+ payload = symbolize_keys(payload)
236
+
237
+ # Warn if user.id is missing
238
+ user = payload[:user]
239
+ if user.nil? || user[:id].nil? || user[:id].to_s.empty?
240
+ warn "[Vortex SDK] Warning: signing payload without user.id means invitations won't be securely attributed to a user."
241
+ end
242
+
243
+ # Parse expiration
244
+ expires_in_seconds = 5 * 60 # Default: 5 minutes
245
+ if options
246
+ options = symbolize_keys(options)
247
+ raw_expires = options[:expires_in] || options[:expiresIn]
248
+ expires_in_seconds = parse_expires_in(raw_expires) if raw_expires
249
+ end
250
+
251
+ # Parse API key and derive signing key
252
+ kid, signing_key = parse_and_derive_key
253
+
254
+ # Build JWT
255
+ now = Time.now.to_i
256
+ exp = now + expires_in_seconds
257
+
258
+ header = { alg: 'HS256', typ: 'JWT', kid: kid }
259
+
260
+ # Build JWT payload
261
+ jwt_payload = {}
262
+
263
+ # Add user if present
264
+ if user
265
+ user_map = {}
266
+ user_map[:id] = user[:id] if user[:id]
267
+ user_map[:email] = user[:email] if user[:email]
268
+ user_map[:name] = user[:name] if user[:name]
269
+ user_map[:avatarUrl] = user[:avatar_url] || user[:avatarUrl] if user[:avatar_url] || user[:avatarUrl]
270
+ user_map[:adminScopes] = user[:admin_scopes] || user[:adminScopes] if user[:admin_scopes] || user[:adminScopes]
271
+ user_map[:allowedEmailDomains] = user[:allowed_email_domains] || user[:allowedEmailDomains] if user[:allowed_email_domains] || user[:allowedEmailDomains]
272
+ jwt_payload[:user] = user_map unless user_map.empty?
273
+ end
274
+
275
+ # Add other payload fields
276
+ jwt_payload[:component] = payload[:component] if payload[:component]
277
+ jwt_payload[:scope] = payload[:scope] if payload[:scope]
278
+ jwt_payload[:vars] = payload[:vars] if payload[:vars] && !payload[:vars].empty?
279
+
280
+ # Add any extra fields from payload (except known keys)
281
+ known_keys = %i[user component scope vars]
282
+ payload.each do |k, v|
283
+ jwt_payload[k] = v unless known_keys.include?(k)
284
+ end
285
+
286
+ # Add JWT claims
287
+ jwt_payload[:iat] = now
288
+ jwt_payload[:exp] = exp
289
+
290
+ # Encode header and payload
291
+ header_b64 = base64url_encode(JSON.generate(header))
292
+ payload_b64 = base64url_encode(JSON.generate(jwt_payload))
293
+
294
+ # Sign
295
+ to_sign = "#{header_b64}.#{payload_b64}"
296
+ signature = OpenSSL::HMAC.digest('SHA256', signing_key, to_sign)
297
+ signature_b64 = base64url_encode(signature)
298
+
299
+ "#{to_sign}.#{signature_b64}"
300
+ rescue => e
301
+ raise VortexError, "Token generation failed: #{e.message}"
302
+ end
303
+
304
+ private
305
+
306
+ # Parse expiration time string or int into seconds.
307
+ # Supports: '5m', '1h', '24h', '7d' or raw seconds as int.
308
+ #
309
+ # @param expires_in [String, Integer] Expiration time
310
+ # @return [Integer] Expiration time in seconds
311
+ # @raise [VortexError] If format is invalid
312
+ def parse_expires_in(expires_in)
313
+ return 5 * 60 if expires_in.nil?
314
+
315
+ if expires_in.is_a?(Integer)
316
+ raise VortexError, "Invalid expires_in value: #{expires_in}. Numeric expires_in must be a positive integer number of seconds." if expires_in <= 0
317
+ return expires_in
318
+ end
319
+
320
+ unless expires_in.is_a?(String)
321
+ raise VortexError, "Invalid expires_in type: #{expires_in.class}. Must be String or Integer."
322
+ end
323
+
324
+ match = expires_in.match(/^(\d+)(m|h|d)$/)
325
+ unless match
326
+ raise VortexError, "Invalid expires_in format: \"#{expires_in}\". Use format like \"5m\", \"1h\", \"24h\", \"7d\" or a number of seconds."
327
+ end
328
+
329
+ value = match[1].to_i
330
+ if value <= 0
331
+ raise VortexError, "Invalid expires_in value: \"#{expires_in}\". Duration must be positive (e.g., \"5m\", \"1h\", \"7d\")."
332
+ end
333
+ unit = match[2]
334
+
335
+ multipliers = { 'm' => 60, 'h' => 60 * 60, 'd' => 60 * 60 * 24 }
336
+ value * multipliers[unit]
337
+ end
338
+
339
+ # Recursively convert hash keys to symbols
340
+ def symbolize_keys(hash)
341
+ return hash unless hash.is_a?(Hash)
342
+ hash.each_with_object({}) do |(k, v), result|
343
+ key = k.is_a?(String) ? k.to_sym : k
344
+ result[key] = v.is_a?(Hash) ? symbolize_keys(v) : v
345
+ end
346
+ end
347
+
121
348
  public
122
349
 
123
350
  # Get invitations by target
@@ -132,7 +359,7 @@ module Vortex
132
359
  req.params['targetValue'] = target_value
133
360
  end
134
361
 
135
- handle_response(response)['invitations'] || []
362
+ transform_invitation_results(handle_response(response)['invitations'] || [])
136
363
  rescue => e
137
364
  raise VortexError, "Failed to get invitations by target: #{e.message}"
138
365
  end
@@ -144,7 +371,7 @@ module Vortex
144
371
  # @raise [VortexError] If the request fails
145
372
  def get_invitation(invitation_id)
146
373
  response = @connection.get("/api/v1/invitations/#{invitation_id}")
147
- handle_response(response)
374
+ transform_invitation_result(handle_response(response))
148
375
  rescue => e
149
376
  raise VortexError, "Failed to get invitation: #{e.message}"
150
377
  end
@@ -236,9 +463,17 @@ module Vortex
236
463
  # Validate that either email or phone is provided
237
464
  raise VortexError, 'User must have either email or phone' if user[:email].nil? && user[:phone].nil?
238
465
 
466
+ # Transform user keys to camelCase for API
467
+ api_user = user.compact.transform_keys do |key|
468
+ case key
469
+ when :is_existing then :isExisting
470
+ else key
471
+ end
472
+ end
473
+
239
474
  body = {
240
475
  invitationIds: invitation_ids,
241
- user: user.compact # Remove nil values
476
+ user: api_user
242
477
  }
243
478
 
244
479
  response = @connection.post('/api/v1/invitations/accept') do |req|
@@ -246,7 +481,7 @@ module Vortex
246
481
  req.body = JSON.generate(body)
247
482
  end
248
483
 
249
- handle_response(response)
484
+ transform_invitation_result(handle_response(response))
250
485
  rescue VortexError
251
486
  raise
252
487
  rescue => e
@@ -271,26 +506,26 @@ module Vortex
271
506
 
272
507
  # Get invitations by group
273
508
  #
274
- # @param group_type [String] The group type
275
- # @param group_id [String] The group ID
509
+ # @param scope_type [String] The group type
510
+ # @param scope [String] The group ID
276
511
  # @return [Array<Hash>] List of invitations for the group
277
512
  # @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}")
513
+ def get_invitations_by_scope(scope_type, scope)
514
+ response = @connection.get("/api/v1/invitations/by-scope/#{scope_type}/#{scope}")
280
515
  result = handle_response(response)
281
- result['invitations'] || []
516
+ transform_invitation_results(result['invitations'] || [])
282
517
  rescue => e
283
518
  raise VortexError, "Failed to get group invitations: #{e.message}"
284
519
  end
285
520
 
286
521
  # Delete invitations by group
287
522
  #
288
- # @param group_type [String] The group type
289
- # @param group_id [String] The group ID
523
+ # @param scope_type [String] The group type
524
+ # @param scope [String] The group ID
290
525
  # @return [Hash] Success response
291
526
  # @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}")
527
+ def delete_invitations_by_scope(scope_type, scope)
528
+ response = @connection.delete("/api/v1/invitations/by-scope/#{scope_type}/#{scope}")
294
529
  handle_response(response)
295
530
  rescue => e
296
531
  raise VortexError, "Failed to delete group invitations: #{e.message}"
@@ -303,7 +538,7 @@ module Vortex
303
538
  # @raise [VortexError] If the request fails
304
539
  def reinvite(invitation_id)
305
540
  response = @connection.post("/api/v1/invitations/#{invitation_id}/reinvite")
306
- handle_response(response)
541
+ transform_invitation_result(handle_response(response))
307
542
  rescue => e
308
543
  raise VortexError, "Failed to reinvite: #{e.message}"
309
544
  end
@@ -322,7 +557,7 @@ module Vortex
322
557
  # @param widget_configuration_id [String] The widget configuration ID to use
323
558
  # @param target [Hash] The invitation target: { type: 'email|sms|internal', value: '...' }
324
559
  # @param inviter [Hash] The inviter info: { user_id: '...', user_email: '...', name: '...' }
325
- # @param groups [Array<Hash>, nil] Optional groups: [{ type: '...', group_id: '...', name: '...' }]
560
+ # @param groups [Array<Hash>, nil] Optional groups: [{ type: '...', scope: '...', name: '...' }]
326
561
  # @param source [String, nil] Optional source for analytics (defaults to 'api')
327
562
  # @param subtype [String, nil] Optional subtype for analytics segmentation (e.g., 'pymk', 'find-friends')
328
563
  # @param template_variables [Hash, nil] Optional template variables for email customization
@@ -336,7 +571,7 @@ module Vortex
336
571
  # 'widget-config-123',
337
572
  # { type: 'email', value: 'invitee@example.com' },
338
573
  # { user_id: 'user-456', user_email: 'inviter@example.com', name: 'John Doe' },
339
- # [{ type: 'team', group_id: 'team-789', name: 'Engineering' }],
574
+ # [{ type: 'team', scope: 'team-789', name: 'Engineering' }],
340
575
  # nil,
341
576
  # nil,
342
577
  # nil,
@@ -351,20 +586,28 @@ module Vortex
351
586
  # nil,
352
587
  # 'pymk'
353
588
  # )
354
- def create_invitation(widget_configuration_id, target, inviter, groups = nil, source = nil, subtype = nil, template_variables = nil, metadata = nil, unfurl_config = nil)
589
+ 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
590
  raise VortexError, 'widget_configuration_id is required' if widget_configuration_id.nil? || widget_configuration_id.empty?
356
591
  raise VortexError, 'target must have type and value' if target[:type].nil? || target[:value].nil?
357
592
  raise VortexError, 'inviter must have user_id' if inviter[:user_id].nil?
358
593
 
594
+ # Scope translation: flat params > scopes > groups
595
+ if scope_id && groups.nil? && scopes.nil?
596
+ groups = [{ type: scope_type || '', scope_id: scope_id, name: scope_name || '' }]
597
+ elsif scopes && groups.nil?
598
+ groups = scopes
599
+ end
600
+
359
601
  # Build request body with camelCase keys for the API
602
+ # Prefer new property names (name/avatar_url), fall back to deprecated (user_name/user_avatar_url)
360
603
  body = {
361
604
  widgetConfigurationId: widget_configuration_id,
362
605
  target: target,
363
606
  inviter: {
364
607
  userId: inviter[:user_id],
365
608
  userEmail: inviter[:user_email],
366
- userName: inviter[:user_name],
367
- userAvatarUrl: inviter[:user_avatar_url]
609
+ name: inviter[:name] || inviter[:user_name],
610
+ avatarUrl: inviter[:avatar_url] || inviter[:user_avatar_url]
368
611
  }.compact
369
612
  }
370
613
 
@@ -372,7 +615,7 @@ module Vortex
372
615
  body[:groups] = groups.map do |g|
373
616
  {
374
617
  type: g[:type],
375
- groupId: g[:group_id],
618
+ groupId: g[:scope] || g[:scope_id] || g[:group_id] || g[:scopeId] || g[:groupId],
376
619
  name: g[:name]
377
620
  }
378
621
  end
@@ -421,7 +664,11 @@ module Vortex
421
664
  encoded_scope = URI.encode_www_form_component(scope)
422
665
 
423
666
  response = @connection.get("/api/v1/invitations/by-scope/#{encoded_scope_type}/#{encoded_scope}/autojoin")
424
- handle_response(response)
667
+ result = handle_response(response)
668
+ if result.is_a?(Hash) && result['invitation'].is_a?(Hash)
669
+ result['invitation'] = transform_invitation_result(result['invitation'])
670
+ end
671
+ result
425
672
  rescue VortexError
426
673
  raise
427
674
  rescue => e
@@ -472,7 +719,11 @@ module Vortex
472
719
  req.body = JSON.generate(body)
473
720
  end
474
721
 
475
- handle_response(response)
722
+ result = handle_response(response)
723
+ if result.is_a?(Hash) && result['invitation'].is_a?(Hash)
724
+ result['invitation'] = transform_invitation_result(result['invitation'])
725
+ end
726
+ result
476
727
  rescue VortexError
477
728
  raise
478
729
  rescue => e
@@ -521,6 +772,29 @@ module Vortex
521
772
 
522
773
  private
523
774
 
775
+ # Transform an invitation scope by adding scope_id (= groupId)
776
+ def transform_scope(scope)
777
+ return scope unless scope.is_a?(Hash) && scope.key?('groupId')
778
+ scope.merge('scopeId' => scope['groupId'])
779
+ end
780
+
781
+ # Transform an invitation result by adding scopes (= groups with scopeId)
782
+ def transform_invitation_result(result)
783
+ return result unless result.is_a?(Hash)
784
+ if result['groups'].is_a?(Array)
785
+ mapped = result['groups'].map { |g| transform_scope(g) }
786
+ result['scopes'] = mapped
787
+ result['groups'] = mapped
788
+ end
789
+ result
790
+ end
791
+
792
+ # Transform an array of invitation results
793
+ def transform_invitation_results(results)
794
+ return results unless results.is_a?(Array)
795
+ results.map { |r| transform_invitation_result(r) }
796
+ end
797
+
524
798
  def build_connection
525
799
  Faraday.new(@base_url) do |conn|
526
800
  conn.request :json
@@ -563,5 +837,17 @@ module Vortex
563
837
  hex = bytes.unpack1('H*')
564
838
  "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
565
839
  end
840
+
841
+ # Deprecated methods for backward compatibility
842
+
843
+ # @deprecated Use get_invitations_by_scope instead
844
+ def get_invitations_by_group(group_type, group)
845
+ get_invitations_by_scope(group_type, group)
846
+ end
847
+
848
+ # @deprecated Use delete_invitations_by_scope instead
849
+ def delete_invitations_by_group(group_type, group)
850
+ delete_invitations_by_scope(group_type, group)
851
+ end
566
852
  end
567
- end
853
+ 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
@@ -142,8 +142,8 @@ module Vortex
142
142
  end
143
143
 
144
144
  # Get invitations by group
145
- # GET /api/vortex/invitations/by-group/:group_type/:group_id
146
- app.get '/api/vortex/invitations/by-group/:group_type/:group_id' do
145
+ # GET /api/vortex/invitations/by-scope/:scope_type/:scope
146
+ app.get '/api/vortex/invitations/by-scope/:scope_type/:scope' do
147
147
  with_vortex_error_handling do
148
148
  user = authenticate_vortex_user
149
149
  return render_unauthorized('Authentication required') unless user
@@ -152,17 +152,17 @@ module Vortex
152
152
  return render_forbidden('Not authorized to get group invitations')
153
153
  end
154
154
 
155
- group_type = params['group_type']
156
- group_id = params['group_id']
155
+ scope_type = params['scope_type']
156
+ scope = params['scope']
157
157
 
158
- invitations = vortex_client.get_invitations_by_group(group_type, group_id)
158
+ invitations = vortex_client.get_invitations_by_scope(scope_type, scope)
159
159
  render_json({ invitations: invitations })
160
160
  end
161
161
  end
162
162
 
163
163
  # Delete invitations by group
164
- # DELETE /api/vortex/invitations/by-group/:group_type/:group_id
165
- app.delete '/api/vortex/invitations/by-group/:group_type/:group_id' do
164
+ # DELETE /api/vortex/invitations/by-scope/:scope_type/:scope
165
+ app.delete '/api/vortex/invitations/by-scope/:scope_type/:scope' do
166
166
  with_vortex_error_handling do
167
167
  user = authenticate_vortex_user
168
168
  return render_unauthorized('Authentication required') unless user
@@ -171,10 +171,10 @@ module Vortex
171
171
  return render_forbidden('Not authorized to delete group invitations')
172
172
  end
173
173
 
174
- group_type = params['group_type']
175
- group_id = params['group_id']
174
+ scope_type = params['scope_type']
175
+ scope = params['scope']
176
176
 
177
- vortex_client.delete_invitations_by_group(group_type, group_id)
177
+ vortex_client.delete_invitations_by_scope(scope_type, scope)
178
178
  render_json({ success: true })
179
179
  end
180
180
  end
data/lib/vortex/types.rb CHANGED
@@ -19,7 +19,7 @@ module Vortex
19
19
  name: String # Required: Group name
20
20
  }.freeze
21
21
 
22
- # InvitationGroup structure from API responses
22
+ # InvitationScope structure from API responses
23
23
  # This matches the MemberGroups table structure from the API
24
24
  # @example
25
25
  # {
@@ -44,12 +44,14 @@ module Vortex
44
44
  # {
45
45
  # email: 'user@example.com',
46
46
  # phone: '+1234567890', # Optional
47
- # name: 'John Doe' # Optional
47
+ # name: 'John Doe', # Optional
48
+ # is_existing: true # Optional - whether user was already registered
48
49
  # }
49
50
  ACCEPT_USER = {
50
- email: String, # Optional but either email or phone must be provided
51
- phone: String, # Optional but either email or phone must be provided
52
- name: String # Optional
51
+ email: String, # Optional but either email or phone must be provided
52
+ phone: String, # Optional but either email or phone must be provided
53
+ name: String, # Optional
54
+ is_existing: 'Boolean (true/false/nil)' # Optional - true if existing user, false if new signup, nil if unknown
53
55
  }.freeze
54
56
 
55
57
  # Invitation structure from API responses
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vortex
4
- VERSION = '1.8.4'
4
+ VERSION = '1.15.0'
5
5
  end