Class: Vortex::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/vortex/client.rb

Overview

Vortex API client for Ruby

Provides the same functionality as other Vortex SDKs with JWT generation, invitation management, and full API compatibility.

Constant Summary collapse

DEFAULT_BASE_URL =

Base URL for Vortex API

'https://api.vortexsoftware.com'

Instance Method Summary collapse

Constructor Details

#initialize(api_key, base_url: nil) ⇒ Client

Returns a new instance of Client.

Parameters:

  • api_key (String)

    Your Vortex API key

  • base_url (String) (defaults to: nil)

    Custom base URL (optional)



20
21
22
23
24
# File 'lib/vortex/client.rb', line 20

def initialize(api_key, base_url: nil)
  @api_key = api_key
  @base_url = base_url || DEFAULT_BASE_URL
  @connection = build_connection
end

Instance Method Details

#accept_invitation(invitation_id, user) ⇒ Hash

Accept a single invitation (recommended method)

This is the recommended method for accepting invitations.

Examples:

user = { email: 'user@example.com', name: 'John Doe' }
result = client.accept_invitation('inv-123', user)

Parameters:

  • invitation_id (String)

    Single invitation ID to accept

  • user (Hash)

    User hash with :email and/or :phone

Returns:

  • (Hash)

    The accepted invitation result

Raises:



505
506
507
# File 'lib/vortex/client.rb', line 505

def accept_invitation(invitation_id, user)
  accept_invitations([invitation_id], user)
end

#accept_invitations(invitation_ids, user_or_target) ⇒ Hash

Accept invitations using the new User format (preferred)

Supports three formats:

  1. User hash (preferred): { email: ‘…’, phone: ‘…’, name: ‘…’ }

  2. Target hash (deprecated): { type: ‘email’, value: ‘…’ }

  3. Array of targets (deprecated): [{ type: ‘email’, value: ‘…’ }, …]

Examples:

New format (preferred)

user = { email: 'user@example.com', name: 'John Doe' }
result = client.accept_invitations(['inv-123'], user)

Legacy format (deprecated)

target = { type: 'email', value: 'user@example.com' }
result = client.accept_invitations(['inv-123'], target)

Parameters:

  • invitation_ids (Array<String>)

    List of invitation IDs to accept

  • user_or_target (Hash, Array)

    User hash with :email/:phone/:name keys, OR legacy target(s)

Returns:

  • (Hash)

    The accepted invitation result

Raises:



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/vortex/client.rb', line 412

def accept_invitations(invitation_ids, user_or_target)
  # Check if it's an array of targets (legacy format with multiple targets)
  if user_or_target.is_a?(Array)
    warn '[Vortex SDK] DEPRECATED: Passing an array of targets is deprecated. ' \
         'Use the User format instead: accept_invitations(invitation_ids, { email: "user@example.com" })'

    raise VortexError, 'No targets provided' if user_or_target.empty?

    last_result = nil
    last_exception = nil

    user_or_target.each do |target|
      begin
        last_result = accept_invitations(invitation_ids, target)
      rescue => e
        last_exception = e
      end
    end

    raise last_exception if last_exception

    return last_result || {}
  end

  # Check if it's a legacy target format (has :type and :value keys)
  is_legacy_target = user_or_target.key?(:type) && user_or_target.key?(:value)

  if is_legacy_target
    warn '[Vortex SDK] DEPRECATED: Passing a target hash is deprecated. ' \
         'Use the User format instead: accept_invitations(invitation_ids, { email: "user@example.com" })'

    # Convert target to User format
    target_type = user_or_target[:type]
    target_value = user_or_target[:value]

    user = {}
    case target_type
    when 'email'
      user[:email] = target_value
    when 'phone', 'phoneNumber'
      user[:phone] = target_value
    else
      # For other types, try to use as email
      user[:email] = target_value
    end

    # Recursively call with User format
    return accept_invitations(invitation_ids, user)
  end

  # New User format
  user = user_or_target

  # Validate that either email or phone is provided
  raise VortexError, 'User must have either email or phone' if user[:email].nil? && user[:phone].nil?

  # Transform user keys to camelCase for API
  api_user = user.compact.transform_keys do |key|
    case key
    when :is_existing then :isExisting
    else key
    end
  end

  body = {
    invitationIds: invitation_ids,
    user: api_user
  }

  response = @connection.post('/api/v1/invitations/accept') do |req|
    req.headers['Content-Type'] = 'application/json'
    req.body = JSON.generate(body)
  end

  transform_invitation_result(handle_response(response))
rescue VortexError
  raise
rescue => e
  raise VortexError, "Failed to accept invitations: #{e.message}"
end

#configure_autojoin(scope, scope_type, domains, component_id, scope_name = nil, metadata = nil) ⇒ Hash

Configure autojoin domains for a specific scope

This endpoint syncs autojoin domains - it will add new domains, remove domains not in the provided list, and deactivate the autojoin invitation if all domains are removed (empty array).

Examples:

result = client.configure_autojoin(
  'acme-org',
  'organization',
  ['acme.com', 'acme.org'],
  'component-123',
  'Acme Corporation'
)

Parameters:

  • scope (String)

    The scope identifier (customer’s group ID)

  • scope_type (String)

    The type of scope (e.g., “organization”, “team”)

  • domains (Array<String>)

    Array of domains to configure for autojoin

  • component_id (String)

    The component ID

  • scope_name (String, nil) (defaults to: nil)

    Optional display name for the scope

  • metadata (Hash, nil) (defaults to: nil)

    Optional metadata to attach to the invitation

Returns:

  • (Hash)

    Response with :autojoin_domains array and :invitation

Raises:



703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
# File 'lib/vortex/client.rb', line 703

def configure_autojoin(scope, scope_type, domains, component_id, scope_name = nil,  = nil)
  raise VortexError, 'scope is required' if scope.nil? || scope.empty?
  raise VortexError, 'scope_type is required' if scope_type.nil? || scope_type.empty?
  raise VortexError, 'component_id is required' if component_id.nil? || component_id.empty?
  raise VortexError, 'domains must be an array' unless domains.is_a?(Array)

  body = {
    scope: scope,
    scopeType: scope_type,
    domains: domains,
    componentId: component_id
  }

  body[:scopeName] = scope_name if scope_name
  body[:metadata] =  if 

  response = @connection.post('/api/v1/invitations/autojoin') do |req|
    req.headers['Content-Type'] = 'application/json'
    req.body = JSON.generate(body)
  end

  result = handle_response(response)
  if result.is_a?(Hash) && result['invitation'].is_a?(Hash)
    result['invitation'] = transform_invitation_result(result['invitation'])
  end
  result
rescue VortexError
  raise
rescue => e
  raise VortexError, "Failed to configure autojoin: #{e.message}"
end

#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) ⇒ Hash

Create an invitation from your backend

This method allows you to create invitations programmatically using your API key, without requiring a user JWT token. Useful for server-side invitation creation, such as “People You May Know” flows or admin-initiated invitations.

Target types:

  • ‘email’: Send an email invitation

  • ‘phone’: Create a phone invitation (short link returned for you to send)

  • ‘internal’: Create an internal invitation for PYMK flows (no email sent)

Examples:

Create an email invitation with custom link preview

result = client.create_invitation(
  'widget-config-123',
  { type: 'email', value: 'invitee@example.com' },
  { user_id: 'user-456', user_email: 'inviter@example.com', name: 'John Doe' },
  [{ type: 'team', scope: 'team-789', name: 'Engineering' }],
  nil,
  nil,
  nil,
  { title: 'Join the team!', description: 'You have been invited', image: 'https://example.com/og.png' }
)

Create an internal invitation (PYMK flow - no email sent)

result = client.create_invitation(
  'widget-config-123',
  { type: 'internal', value: 'internal-user-abc' },
  { user_id: 'user-456' },
  nil,
  'pymk'
)

Parameters:

  • widget_configuration_id (String)

    The widget configuration ID to use

  • target (Hash)

    The invitation target: { type: ‘email|sms|internal’, value: ‘…’ }

  • inviter (Hash)

    The inviter info: { user_id: ‘…’, user_email: ‘…’, name: ‘…’ }

  • groups (Array<Hash>, nil) (defaults to: nil)

    Optional groups: [{ type: ‘…’, scope: ‘…’, name: ‘…’ }]

  • source (String, nil) (defaults to: nil)

    Optional source for analytics (defaults to ‘api’)

  • subtype (String, nil) (defaults to: nil)

    Optional subtype for analytics segmentation (e.g., ‘pymk’, ‘find-friends’)

  • template_variables (Hash, nil) (defaults to: nil)

    Optional template variables for email customization

  • metadata (Hash, nil) (defaults to: nil)

    Optional metadata passed through to webhooks

  • unfurl_config (Hash, nil) (defaults to: nil)

    Optional link unfurl (Open Graph) config: { title: ‘…’, description: ‘…’, image: ‘…’, type: ‘…’, site_name: ‘…’ }

Returns:

  • (Hash)

    Created invitation with :id, :short_link, :status, :created_at

Raises:



591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
# File 'lib/vortex/client.rb', line 591

def create_invitation(widget_configuration_id, target, inviter, groups = nil, source = nil, subtype = nil, template_variables = nil,  = nil, unfurl_config = nil, scopes: nil, scope_id: nil, scope_type: nil, scope_name: nil)
  raise VortexError, 'widget_configuration_id is required' if widget_configuration_id.nil? || widget_configuration_id.empty?
  raise VortexError, 'target must have type and value' if target[:type].nil? || target[:value].nil?
  raise VortexError, 'inviter must have user_id' if inviter[:user_id].nil?

  # Scope translation: flat params > scopes > groups
  if scope_id && groups.nil? && scopes.nil?
    groups = [{ type: scope_type || '', scope_id: scope_id, name: scope_name || '' }]
  elsif scopes && groups.nil?
    groups = scopes
  end

  # Build request body with camelCase keys for the API
  # Prefer new property names (name/avatar_url), fall back to deprecated (user_name/user_avatar_url)
  body = {
    widgetConfigurationId: widget_configuration_id,
    target: target,
    inviter: {
      userId: inviter[:user_id],
      userEmail: inviter[:user_email],
      name: inviter[:name] || inviter[:user_name],
      avatarUrl: inviter[:avatar_url] || inviter[:user_avatar_url]
    }.compact
  }

  if groups && !groups.empty?
    body[:groups] = groups.map do |g|
      {
        type: g[:type],
        groupId: g[:scope] || g[:scope_id] || g[:group_id] || g[:scopeId] || g[:groupId],
        name: g[:name]
      }
    end
  end

  body[:source] = source if source
  body[:subtype] = subtype if subtype
  body[:templateVariables] = template_variables if template_variables
  body[:metadata] =  if 
  if unfurl_config
    body[:unfurlConfig] = {
      title: unfurl_config[:title],
      description: unfurl_config[:description],
      image: unfurl_config[:image],
      type: unfurl_config[:type],
      siteName: unfurl_config[:site_name]
    }.compact
  end

  response = @connection.post('/api/v1/invitations') do |req|
    req.headers['Content-Type'] = 'application/json'
    req.body = JSON.generate(body)
  end

  handle_response(response)
rescue VortexError
  raise
rescue => e
  raise VortexError, "Failed to create invitation: #{e.message}"
end

#delete_invitations_by_scope(scope_type, scope) ⇒ Hash

Delete invitations by group

Parameters:

  • scope_type (String)

    The group type

  • scope (String)

    The group ID

Returns:

  • (Hash)

    Success response

Raises:



529
530
531
532
533
534
# File 'lib/vortex/client.rb', line 529

def delete_invitations_by_scope(scope_type, scope)
  response = @connection.delete("/api/v1/invitations/by-scope/#{scope_type}/#{scope}")
  handle_response(response)
rescue => e
  raise VortexError, "Failed to delete group invitations: #{e.message}"
end

#generate_jwt(params, options = {}) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/vortex/client.rb', line 125

def generate_jwt(params, options = {})
  user = params[:user]
  attributes = params[:attributes]

  # Parse API key - same format as Node.js SDK
  prefix, encoded_id, key = @api_key.split('.')

  raise VortexError, 'Invalid API key format' unless prefix && encoded_id && key
  raise VortexError, 'Invalid API key prefix' unless prefix == 'VRTX'

  # Decode the ID from base64url (same as Node.js Buffer.from(encodedId, 'base64url'))
  decoded_bytes = Base64.urlsafe_decode64(encoded_id)

  # Convert to UUID string format (same as uuidStringify in Node.js)
  id = format_uuid(decoded_bytes)

  raw_expires = options[:expires_in] || options[:expiresIn]
  expires_in_seconds = raw_expires ? parse_expires_in(raw_expires) : 2592000 # 30 days
  expires = Time.now.to_i + expires_in_seconds

  # Step 1: Derive signing key from API key + ID (same as Node.js)
  signing_key = OpenSSL::HMAC.digest('sha256', key, id)

  # Step 2: Build header + payload
  header = {
    iat: Time.now.to_i,
    alg: 'HS256',
    typ: 'JWT',
    kid: id
  }

  # Build payload - start with required fields
  payload = {
    userId: user[:id],
    userEmail: user[:email],
    expires: expires
  }

  # Add name if present (prefer new property, fall back to deprecated)
  user_name = user[:name] || user[:user_name]
  if user_name
    payload[:name] = user_name
  end

  # Add avatarUrl if present (prefer new property, fall back to deprecated)
  user_avatar_url = user[:avatar_url] || user[:user_avatar_url]
  if user_avatar_url
    payload[:avatarUrl] = user_avatar_url
  end

  # Add adminScopes if present
  if user[:admin_scopes]
    payload[:adminScopes] = user[:admin_scopes]
  end

  # Add allowedEmailDomains if present (for domain-restricted invitations)
  if user[:allowed_email_domains] && !user[:allowed_email_domains].empty?
    payload[:allowedEmailDomains] = user[:allowed_email_domains]
  end

  # Add any additional properties from attributes
  if attributes && !attributes.empty?
    payload.merge!(attributes)
  end

  # Step 3: Base64URL encode (same as Node.js)
  header_b64 = base64url_encode(JSON.generate(header))
  payload_b64 = base64url_encode(JSON.generate(payload))

  # Step 4: Sign with HMAC-SHA256 (same as Node.js)
  signature = OpenSSL::HMAC.digest('sha256', signing_key, "#{header_b64}.#{payload_b64}")
  signature_b64 = base64url_encode(signature)

  "#{header_b64}.#{payload_b64}.#{signature_b64}"
rescue => e
  raise VortexError, "JWT generation failed: #{e.message}"
end

#generate_token(payload, options = nil) ⇒ String

Generate a signed token for use with Vortex widgets.

This method generates a signed JWT token containing your payload data. The token can be passed to widgets via the ‘token` prop to authenticate and authorize the request.

Examples:

Sign just the user (minimum for secure attribution)

token = client.generate_token({ user: { id: 'user-123' } })

Sign full payload

token = client.generate_token({
  component: 'widget-abc',
  user: { id: 'user-123', name: 'Peter', email: 'peter@example.com' },
  scope: 'workspace_456',
  vars: { company_name: 'Acme' }
})

Custom expiration (default is 30 days)

token = client.generate_token(payload, { expires_in: '1h' })
token = client.generate_token(payload, { expires_in: 3600 }) # seconds

Parameters:

  • payload (Hash)

    Data to sign (user, component, scope, vars, etc.) At minimum, include user for secure invitation attribution.

  • options (Hash) (defaults to: nil)

    Optional configuration

Options Hash (options):

  • :expires_in (String, Integer)

    Expiration time (default: 30 days) Can be a duration string (“5m”, “1h”, “24h”, “7d”) or seconds as integer.

Returns:

  • (String)

    Signed JWT token

Raises:

  • (VortexError)

    If API key format is invalid or token generation fails



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/vortex/client.rb', line 231

def generate_token(payload, options = nil)
  # Validate inputs
  raise VortexError, "payload must be a Hash" unless payload.is_a?(Hash)
  raise VortexError, "options must be a Hash or nil" if options && !options.is_a?(Hash)

  # Normalize payload keys to symbols
  payload = symbolize_keys(payload)

  # Warn if user.id is missing
  user = payload[:user]
  if user.nil? || user[:id].nil? || user[:id].to_s.empty?
    warn "[Vortex SDK] Warning: signing payload without user.id means invitations won't be securely attributed to a user."
  end

  # Parse expiration
  expires_in_seconds = 30 * 24 * 60 * 60 # Default: 30 days
  if options
    options = symbolize_keys(options)
    raw_expires = options[:expires_in] || options[:expiresIn]
    expires_in_seconds = parse_expires_in(raw_expires) if raw_expires
  end

  # Parse API key and derive signing key
  kid, signing_key = parse_and_derive_key

  # Build JWT
  now = Time.now.to_i
  exp = now + expires_in_seconds

  header = { alg: 'HS256', typ: 'JWT', kid: kid }

  # Build JWT payload
  jwt_payload = {}

  # Add user if present
  if user
    user_map = {}
    user_map[:id] = user[:id] if user[:id]
    user_map[:email] = user[:email] if user[:email]
    user_map[:name] = user[:name] if user[:name]
    user_map[:avatarUrl] = user[:avatar_url] || user[:avatarUrl] if user[:avatar_url] || user[:avatarUrl]
    user_map[:adminScopes] = user[:admin_scopes] || user[:adminScopes] if user[:admin_scopes] || user[:adminScopes]
    user_map[:allowedEmailDomains] = user[:allowed_email_domains] || user[:allowedEmailDomains] if user[:allowed_email_domains] || user[:allowedEmailDomains]
    jwt_payload[:user] = user_map unless user_map.empty?
  end

  # Add other payload fields
  jwt_payload[:component] = payload[:component] if payload[:component]
  jwt_payload[:scope] = payload[:scope] if payload[:scope]
  jwt_payload[:vars] = payload[:vars] if payload[:vars] && !payload[:vars].empty?

  # Add any extra fields from payload (except known keys)
  known_keys = %i[user component scope vars]
  payload.each do |k, v|
    jwt_payload[k] = v unless known_keys.include?(k)
  end

  # Add JWT claims
  jwt_payload[:iat] = now
  jwt_payload[:exp] = exp

  # Encode header and payload
  header_b64 = base64url_encode(JSON.generate(header))
  payload_b64 = base64url_encode(JSON.generate(jwt_payload))

  # Sign
  to_sign = "#{header_b64}.#{payload_b64}"
  signature = OpenSSL::HMAC.digest('SHA256', signing_key, to_sign)
  signature_b64 = base64url_encode(signature)

  "#{to_sign}.#{signature_b64}"
rescue => e
  raise VortexError, "Token generation failed: #{e.message}"
end

#get_autojoin_domains(scope_type, scope) ⇒ Hash

Get autojoin domains configured for a specific scope

Examples:

result = client.get_autojoin_domains('organization', 'acme-org')
result['autojoinDomains'].each do |domain|
  puts "Domain: #{domain['domain']}"
end

Parameters:

  • scope_type (String)

    The type of scope (e.g., “organization”, “team”, “project”)

  • scope (String)

    The scope identifier (customer’s group ID)

Returns:

  • (Hash)

    Response with :autojoin_domains array and :invitation

Raises:



664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
# File 'lib/vortex/client.rb', line 664

def get_autojoin_domains(scope_type, scope)
  encoded_scope_type = URI.encode_www_form_component(scope_type)
  encoded_scope = URI.encode_www_form_component(scope)

  response = @connection.get("/api/v1/invitations/by-scope/#{encoded_scope_type}/#{encoded_scope}/autojoin")
  result = handle_response(response)
  if result.is_a?(Hash) && result['invitation'].is_a?(Hash)
    result['invitation'] = transform_invitation_result(result['invitation'])
  end
  result
rescue VortexError
  raise
rescue => e
  raise VortexError, "Failed to get autojoin domains: #{e.message}"
end

#get_invitation(invitation_id) ⇒ Hash

Get a specific invitation by ID

Parameters:

  • invitation_id (String)

    The invitation ID

Returns:

  • (Hash)

    The invitation data

Raises:



374
375
376
377
378
379
# File 'lib/vortex/client.rb', line 374

def get_invitation(invitation_id)
  response = @connection.get("/api/v1/invitations/#{invitation_id}")
  transform_invitation_result(handle_response(response))
rescue => e
  raise VortexError, "Failed to get invitation: #{e.message}"
end

#get_invitations_by_scope(scope_type, scope) ⇒ Array<Hash>

Get invitations by group

Parameters:

  • scope_type (String)

    The group type

  • scope (String)

    The group ID

Returns:

  • (Array<Hash>)

    List of invitations for the group

Raises:



515
516
517
518
519
520
521
# File 'lib/vortex/client.rb', line 515

def get_invitations_by_scope(scope_type, scope)
  response = @connection.get("/api/v1/invitations/by-scope/#{scope_type}/#{scope}")
  result = handle_response(response)
  transform_invitation_results(result['invitations'] || [])
rescue => e
  raise VortexError, "Failed to get group invitations: #{e.message}"
end

#get_invitations_by_target(target_type, target_value) ⇒ Array<Hash>

Get invitations by target

Parameters:

  • target_type (String)

    Type of target (email, sms)

  • target_value (String)

    Value of target (email address, phone number)

Returns:

  • (Array<Hash>)

    List of invitations

Raises:



358
359
360
361
362
363
364
365
366
367
# File 'lib/vortex/client.rb', line 358

def get_invitations_by_target(target_type, target_value)
  response = @connection.get('/api/v1/invitations') do |req|
    req.params['targetType'] = target_type
    req.params['targetValue'] = target_value
  end

  transform_invitation_results(handle_response(response)['invitations'] || [])
rescue => e
  raise VortexError, "Failed to get invitations by target: #{e.message}"
end

#reinvite(invitation_id) ⇒ Hash

Reinvite a user

Parameters:

  • invitation_id (String)

    The invitation ID to reinvite

Returns:

  • (Hash)

    The reinvited invitation result

Raises:



541
542
543
544
545
546
# File 'lib/vortex/client.rb', line 541

def reinvite(invitation_id)
  response = @connection.post("/api/v1/invitations/#{invitation_id}/reinvite")
  transform_invitation_result(handle_response(response))
rescue => e
  raise VortexError, "Failed to reinvite: #{e.message}"
end

#revoke_invitation(invitation_id) ⇒ Hash

Revoke (delete) an invitation

Parameters:

  • invitation_id (String)

    The invitation ID to revoke

Returns:

  • (Hash)

    Success response

Raises:



386
387
388
389
390
391
# File 'lib/vortex/client.rb', line 386

def revoke_invitation(invitation_id)
  response = @connection.delete("/api/v1/invitations/#{invitation_id}")
  handle_response(response)
rescue => e
  raise VortexError, "Failed to revoke invitation: #{e.message}"
end

#sign(user) ⇒ String

Generate a JWT token for a user

Sign a user object for use with the widget signature prop.

Examples:

Simple usage

client = Vortex::Client.new(ENV['VORTEX_API_KEY'])
jwt = client.generate_jwt({
  user: {
    id: 'user-123',
    email: 'user@example.com',
    admin_scopes: ['autojoin']
  }
})

With additional attributes

jwt = client.generate_jwt({
  user: { id: 'user-123', email: 'user@example.com' },
  attributes: { role: 'admin', department: 'Engineering' }
})
client = Vortex::Client.new(ENV['VORTEX_API_KEY'])
signature = client.sign({ id: 'user-123', email: 'user@example.com' })
# Pass signature to frontend alongside user prop

Parameters:

  • params (Hash)

    JWT parameters with :user (required) and optional :attributes

  • user (Hash)

    User data with :id, :email, etc.

Returns:

  • (String)

    JWT token

  • (String)

    Signature in “kid:hexDigest” format

Raises:

  • (VortexError)

    If API key is invalid or JWT generation fails



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/vortex/client.rb', line 56

def sign(user)
  user_id = user[:id] || user['id']
  raise VortexError, 'User must have an :id field' if user_id.nil? || user_id.to_s.empty?

  kid, signing_key = parse_and_derive_key

  # Build canonical payload — include ALL user fields with key normalization
  key_map = { id: 'userId', email: 'userEmail',
              admin_scopes: 'adminScopes', allowed_email_domains: 'allowedEmailDomains' }
  canonical = {}
  user.each do |k, v|
    str_key = k.to_s
    mapped = key_map[k] || key_map[k.to_s.to_sym]
    if mapped
      canonical[mapped] = v
    elsif str_key == 'id'
      canonical['userId'] = v
    elsif str_key == 'email'
      canonical['userEmail'] = v
    elsif !%w[name user_name avatar_url user_avatar_url].include?(str_key)
      # Skip name/avatar fields here - handle them explicitly below
      canonical[str_key] = v
    end
  end
  canonical['userId'] = user_id  # ensure normalized

  # Prefer new property names (name/avatar_url), fall back to deprecated (user_name/user_avatar_url)
  user_name = user[:name] || user[:user_name]
  canonical['name'] = user_name if user_name

  user_avatar_url = user[:avatar_url] || user[:user_avatar_url]
  canonical['avatarUrl'] = user_avatar_url if user_avatar_url

  # Recursive canonical JSON (sorted keys at every level)
  canonical_json = JSON.generate(canonicalize_value(canonical))

  digest = OpenSSL::HMAC.hexdigest('SHA256', signing_key, canonical_json)
  "#{kid}:#{digest}"
end

#sync_internal_invitation(creator_id, target_value, action, component_id) ⇒ Hash

Sync an internal invitation action (accept or decline)

This method notifies Vortex that an internal invitation was accepted or declined within your application, so Vortex can update the invitation status accordingly.

Examples:

result = client.sync_internal_invitation(
  'user-123',
  'user-456',
  'accepted',
  'component-uuid-789'
)
puts "Processed #{result['processed']} invitations"

Parameters:

  • creator_id (String)

    The inviter’s user ID

  • target_value (String)

    The invitee’s user ID

  • action (String)

    The action taken: “accepted” or “declined”

  • component_id (String)

    The widget component UUID

Returns:

  • (Hash)

    Response with :processed count and :invitationIds array

Raises:



755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
# File 'lib/vortex/client.rb', line 755

def sync_internal_invitation(creator_id, target_value, action, component_id)
  body = {
    creatorId: creator_id,
    targetValue: target_value,
    action: action,
    componentId: component_id
  }

  response = @connection.post('/api/v1/invitations/sync-internal-invitation') do |req|
    req.headers['Content-Type'] = 'application/json'
    req.body = JSON.generate(body)
  end

  handle_response(response)
rescue VortexError
  raise
rescue => e
  raise VortexError, "Failed to sync internal invitation: #{e.message}"
end