vortex-ruby-sdk 1.9.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8665551bcdf4cf5b7ab05fc5b2dd880b9e1166dd4f544acceb864365e86c8db
4
- data.tar.gz: 82d89462684d03e6b26769f20b8a44ba3f4fe71cf729ce78f54268f070fa36df
3
+ metadata.gz: 2fa76d2aa711c84370eb93e563946ee45c78c12d2a5c54d6b4de13ebb49f3c6a
4
+ data.tar.gz: 74e291d07ca213da127b5cb35e8964e63664d101ffff8fe06aa3e586b8efc42f
5
5
  SHA512:
6
- metadata.gz: 32a6d0a546f91c5165f43af6ac0ed995386b92d267006eb6793b865676fe0bcfe6631e748294ecf589253d7621df492aff7b442881266f7906dc6833b78be63e
7
- data.tar.gz: 39253da9f792aacc194e619eb4ab00104d15ce7901ca5345a82e1681f3aa2d0e0bb268c6b832fad3537736641378f57fe3ee2ae31a5af8b2157ea5a8a2b02c17
6
+ metadata.gz: ef4b13b91543a141f041d2bfcc812b48e55a63998a7d8da7e445107daec73bb10e3d047532dfce015ed51a8160ddf5df28f78ca46285311fefa3a6f692ad40af
7
+ data.tar.gz: 9a8723995e0cdfae388688385544242e30c2c280192938ac59aebfa724f5ebf0f3508ea1eb44765c92d8ec936885bdef30ee0b828fab6ecc4c4746b29a19860f
@@ -5,10 +5,12 @@
5
5
  **Requires:** Ruby 3.0+
6
6
 
7
7
  ## Prerequisites
8
+
8
9
  From integration contract you need: API endpoint prefix, scope entity, authentication pattern
9
10
  From discovery data you need: Ruby framework (Rails, Sinatra), database ORM, auth pattern
10
11
 
11
12
  ## Key Facts
13
+
12
14
  - Framework-agnostic Ruby SDK
13
15
  - Client-based: instantiate `Vortex::Client` class and call methods
14
16
  - Built-in Rails and Sinatra helpers
@@ -20,11 +22,13 @@ From discovery data you need: Ruby framework (Rails, Sinatra), database ORM, aut
20
22
  ## Step 1: Install
21
23
 
22
24
  Add to `Gemfile`:
25
+
23
26
  ```ruby
24
27
  gem 'vortex-ruby-sdk'
25
28
  ```
26
29
 
27
30
  Then install:
31
+
28
32
  ```bash
29
33
  bundle install
30
34
  ```
@@ -49,6 +53,7 @@ VORTEX_API_KEY=VRTX.your-api-key-here.secret
49
53
  ## Step 3: Create Vortex Client
50
54
 
51
55
  ### Rails Initializer (`config/initializers/vortex.rb`):
56
+
52
57
  ```ruby
53
58
  Rails.application.config.vortex = Vortex::Client.new(
54
59
  Rails.application.credentials.vortex_api_key || ENV['VORTEX_API_KEY']
@@ -56,6 +61,7 @@ Rails.application.config.vortex = Vortex::Client.new(
56
61
  ```
57
62
 
58
63
  ### Rails Concern (`app/controllers/concerns/vortex_helper.rb`):
64
+
59
65
  ```ruby
60
66
  module VortexHelper
61
67
  extend ActiveSupport::Concern
@@ -71,6 +77,7 @@ end
71
77
  ```
72
78
 
73
79
  ### Sinatra Configuration:
80
+
74
81
  ```ruby
75
82
  # app.rb or config.ru
76
83
  require 'sinatra/base'
@@ -94,6 +101,7 @@ end
94
101
  ## Step 4: Extract Authenticated User
95
102
 
96
103
  ### Rails with Devise:
104
+
97
105
  ```ruby
98
106
  # app/controllers/application_controller.rb
99
107
  class ApplicationController < ActionController::API
@@ -116,6 +124,7 @@ end
116
124
  ```
117
125
 
118
126
  ### Rails with JWT:
127
+
119
128
  ```ruby
120
129
  class ApplicationController < ActionController::API
121
130
  before_action :authenticate_user_from_token!
@@ -147,6 +156,7 @@ end
147
156
  ```
148
157
 
149
158
  ### Sinatra:
159
+
150
160
  ```ruby
151
161
  # app/helpers/auth_helper.rb
152
162
  module AuthHelper
@@ -185,6 +195,7 @@ end
185
195
  ```
186
196
 
187
197
  **Adapt to their patterns:**
198
+
188
199
  - Match their auth mechanism (Devise, JWT, sessions)
189
200
  - Match their user structure
190
201
  - Match their admin detection logic
@@ -194,6 +205,7 @@ end
194
205
  ## Step 5: Implement JWT Generation Endpoint
195
206
 
196
207
  ### Rails (`app/controllers/vortex_controller.rb`):
208
+
197
209
  ```ruby
198
210
  class VortexController < ApplicationController
199
211
  before_action :require_authentication!
@@ -217,6 +229,7 @@ end
217
229
  ```
218
230
 
219
231
  ### Sinatra:
232
+
220
233
  ```ruby
221
234
  require 'sinatra/base'
222
235
  require 'json'
@@ -256,6 +269,7 @@ end
256
269
  ## Step 6: Implement Accept Invitations Endpoint (CRITICAL)
257
270
 
258
271
  ### Rails Routes (`config/routes.rb`):
272
+
259
273
  ```ruby
260
274
  Rails.application.routes.draw do
261
275
  namespace :api do
@@ -272,6 +286,7 @@ end
272
286
  ```
273
287
 
274
288
  ### Rails with ActiveRecord:
289
+
275
290
  ```ruby
276
291
  class Api::VortexController < ApplicationController
277
292
  before_action :require_authentication!
@@ -292,11 +307,11 @@ class Api::VortexController < ApplicationController
292
307
  invitation_ids.each do |invitation_id|
293
308
  invitation = vortex_client.get_invitation(invitation_id)
294
309
 
295
- (invitation['groups'] || []).each do |group|
296
- GroupMembership.find_or_create_by!(
310
+ (invitation["groups"] || []).each do |scope|
311
+ ScopeMembership.find_or_create_by!(
297
312
  user_id: current_user.id,
298
- group_type: group['type'],
299
- group_id: group['groupId']
313
+ scope_type: scope['type'],
314
+ scope_id: scope['groupId']
300
315
  ) do |membership|
301
316
  membership.role = invitation['role'] || 'member'
302
317
  end
@@ -320,6 +335,7 @@ end
320
335
  ```
321
336
 
322
337
  ### Sinatra with Sequel:
338
+
323
339
  ```ruby
324
340
  post '/api/vortex/invitations/accept' do
325
341
  require_authentication!
@@ -340,14 +356,14 @@ post '/api/vortex/invitations/accept' do
340
356
  invitation_ids.each do |invitation_id|
341
357
  invitation = vortex.get_invitation(invitation_id)
342
358
 
343
- (invitation['groups'] || []).each do |group|
344
- GroupMembership.insert_conflict(
345
- target: [:user_id, :group_type, :group_id],
359
+ (invitation["groups"] || []).each do |scope|
360
+ ScopeMembership.insert_conflict(
361
+ target: [:user_id, :scope_type, :scope_id],
346
362
  update: { role: invitation['role'] || 'member' }
347
363
  ).insert(
348
364
  user_id: current_user.id,
349
- group_type: group['type'],
350
- group_id: group['groupId'],
365
+ scope_type: scope['type'],
366
+ scope_id: scope['groupId'],
351
367
  role: invitation['role'] || 'member',
352
368
  joined_at: Time.now
353
369
  )
@@ -368,6 +384,7 @@ end
368
384
  ```
369
385
 
370
386
  **Critical - Adapt database logic:**
387
+
371
388
  - Use their actual table/model names (from discovery)
372
389
  - Use their actual field names
373
390
  - Use their ORM pattern (ActiveRecord, Sequel)
@@ -378,19 +395,20 @@ end
378
395
  ## Step 7: Database Models
379
396
 
380
397
  ### Rails Migration:
398
+
381
399
  ```ruby
382
- # db/migrate/YYYYMMDDHHMMSS_create_group_memberships.rb
383
- class CreateGroupMemberships < ActiveRecord::Migration[7.0]
400
+ # db/migrate/YYYYMMDDHHMMSS_create_scope_memberships.rb
401
+ class CreateScopeMemberships < ActiveRecord::Migration[7.0]
384
402
  def change
385
- create_table :group_memberships do |t|
403
+ create_table :scope_memberships do |t|
386
404
  t.string :user_id, null: false
387
- t.string :group_type, null: false, limit: 100
388
- t.string :group_id, null: false
405
+ t.string :scope_type, null: false, limit: 100
406
+ t.string :scope_id, null: false
389
407
  t.string :role, default: 'member', limit: 50
390
408
  t.timestamp :joined_at, null: false, default: -> { 'CURRENT_TIMESTAMP' }
391
409
 
392
- t.index [:user_id, :group_type, :group_id], unique: true, name: 'unique_membership'
393
- t.index [:group_type, :group_id], name: 'idx_group'
410
+ t.index [:user_id, :scope_type, :scope_id], unique: true, name: 'unique_membership'
411
+ t.index [:scope_type, :scope_id], name: 'idx_scope'
394
412
  t.index [:user_id], name: 'idx_user'
395
413
  end
396
414
  end
@@ -398,33 +416,35 @@ end
398
416
  ```
399
417
 
400
418
  ### Rails Model:
419
+
401
420
  ```ruby
402
- # app/models/group_membership.rb
403
- class GroupMembership < ApplicationRecord
421
+ # app/models/scope_membership.rb
422
+ class ScopeMembership < ApplicationRecord
404
423
  validates :user_id, presence: true
405
- validates :group_type, presence: true
406
- validates :group_id, presence: true
424
+ validates :scope_type, presence: true
425
+ validates :scope_id, presence: true
407
426
  validates :role, presence: true
408
427
 
409
- validates :user_id, uniqueness: { scope: [:group_type, :group_id] }
428
+ validates :user_id, uniqueness: { scope: [:scope_type, :scope_id] }
410
429
  end
411
430
  ```
412
431
 
413
432
  ### Sequel Migration:
433
+
414
434
  ```ruby
415
- # db/migrations/001_create_group_memberships.rb
435
+ # db/migrations/001_create_scope_memberships.rb
416
436
  Sequel.migration do
417
437
  change do
418
- create_table(:group_memberships) do
438
+ create_table(:scope_memberships) do
419
439
  primary_key :id
420
440
  String :user_id, null: false
421
- String :group_type, size: 100, null: false
422
- String :group_id, null: false
441
+ String :scope_type, size: 100, null: false
442
+ String :scope_id, null: false
423
443
  String :role, size: 50, default: 'member'
424
444
  DateTime :joined_at, null: false, default: Sequel::CURRENT_TIMESTAMP
425
445
 
426
- index [:user_id, :group_type, :group_id], unique: true, name: :unique_membership
427
- index [:group_type, :group_id], name: :idx_group
446
+ index [:user_id, :scope_type, :scope_id], unique: true, name: :unique_membership
447
+ index [:scope_type, :scope_id], name: :idx_scope
428
448
  index [:user_id], name: :idx_user
429
449
  end
430
450
  end
@@ -450,6 +470,7 @@ curl -X POST http://localhost:3000/api/vortex/jwt \
450
470
  ```
451
471
 
452
472
  Expected response:
473
+
453
474
  ```json
454
475
  {
455
476
  "jwt": "eyJhbGciOiJIUzI1NiIs..."
@@ -471,6 +492,7 @@ Expected response:
471
492
  **CORS errors** → Add CORS middleware:
472
493
 
473
494
  **Rails:**
495
+
474
496
  ```ruby
475
497
  # Gemfile
476
498
  gem 'rack-cors'
@@ -485,6 +507,7 @@ end
485
507
  ```
486
508
 
487
509
  **Sinatra:**
510
+
488
511
  ```ruby
489
512
  require 'sinatra/cross_origin'
490
513
 
@@ -508,13 +531,15 @@ end
508
531
  ## After Implementation Report
509
532
 
510
533
  List files created/modified:
534
+
511
535
  - Dependency: Gemfile
512
536
  - Client: config/initializers/vortex.rb (or concern)
513
537
  - Controller: app/controllers/vortex_controller.rb (or Sinatra routes)
514
- - Model: app/models/group_membership.rb
515
- - Migration: db/migrate/XXX_create_group_memberships.rb
538
+ - Model: app/models/scope_membership.rb
539
+ - Migration: db/migrate/XXX_create_scope_memberships.rb
516
540
 
517
541
  Confirm:
542
+
518
543
  - Vortex gem installed
519
544
  - VortexClient instance created
520
545
  - JWT endpoint returns valid JWT
@@ -525,6 +550,7 @@ Confirm:
525
550
  ## Endpoints Registered
526
551
 
527
552
  All endpoints at `/api/vortex`:
553
+
528
554
  - `POST /jwt` - Generate JWT for authenticated user
529
555
  - `GET /invitations` - Get invitations by target
530
556
  - `GET /invitations/:id` - Get invitation by ID
data/README.md CHANGED
@@ -61,8 +61,37 @@ invitations = client.get_invitations_by_target('email', 'user@example.com')
61
61
  # Accept an invitation
62
62
  client.accept_invitation('inv-123', { email: 'user@example.com' })
63
63
 
64
- # Get invitations by group
65
- group_invitations = client.get_invitations_by_group('team', 'team1')
64
+ # Accept with new vs existing user tracking
65
+ # is_existing: true if user was already registered, false if new signup
66
+ client.accept_invitation('inv-123', {
67
+ email: 'user@example.com',
68
+ is_existing: false # New user signup
69
+ })
70
+
71
+ # Get invitations by scope
72
+ scope_invitations = client.get_invitations_by_scope('team', 'team1')
73
+ ```
74
+
75
+ ## Token Generation (for Widgets)
76
+
77
+ Use `generate_token` for widget authentication. This method generates a signed JWT token that can be passed to Vortex widgets via the `token` prop.
78
+
79
+ ```ruby
80
+ # Sign just the user (minimum for secure invitation attribution)
81
+ token = client.generate_token({ user: { id: 'user-123' } })
82
+
83
+ # Sign full payload with component, scope, and variables
84
+ token = client.generate_token({
85
+ component: 'widget-abc',
86
+ user: { id: 'user-123', name: 'Peter', email: 'peter@example.com' },
87
+ scope: 'workspace_456',
88
+ vars: { company_name: 'Acme' }
89
+ })
90
+
91
+ # Custom expiration (default is 5 minutes)
92
+ payload = { user: { id: 'user-123' } }
93
+ token = client.generate_token(payload, { expires_in: '1h' }) # Supports "5m", "1h", "24h", "7d"
94
+ token = client.generate_token(payload, { expires_in: 3600 }) # Or seconds as integer
66
95
  ```
67
96
 
68
97
  ## Rails Integration
@@ -116,8 +145,8 @@ Rails.application.routes.draw do
116
145
  get 'invitations/:invitation_id', action: 'get_invitation'
117
146
  delete 'invitations/:invitation_id', action: 'revoke_invitation'
118
147
  post 'invitations/accept', action: 'accept_invitations'
119
- get 'invitations/by-group/:group_type/:group_id', action: 'get_invitations_by_group'
120
- delete 'invitations/by-group/:group_type/:group_id', action: 'delete_invitations_by_group'
148
+ get 'invitations/by-scope/:scope_type/:scope', action: 'get_invitations_by_scope'
149
+ delete 'invitations/by-scope/:scope_type/:scope', action: 'delete_invitations_by_scope'
121
150
  post 'invitations/:invitation_id/reinvite', action: 'reinvite'
122
151
  end
123
152
  end
@@ -171,8 +200,8 @@ All methods match the functionality of other Vortex SDKs:
171
200
  - `get_invitation(invitation_id)` - Get specific invitation
172
201
  - `revoke_invitation(invitation_id)` - Revoke invitation
173
202
  - `accept_invitation(invitation_id, user)` - Accept an invitation
174
- - `get_invitations_by_group(group_type, group_id)` - Get group invitations
175
- - `delete_invitations_by_group(group_type, group_id)` - Delete group invitations
203
+ - `get_invitations_by_scope(scope_type, scope)` - Get scope invitations
204
+ - `delete_invitations_by_scope(scope_type, scope)` - Delete scope invitations
176
205
  - `reinvite(invitation_id)` - Reinvite user
177
206
  - `sync_internal_invitation(creator_id, target_value, action, component_id)` - Sync internal invitation action
178
207
 
@@ -182,12 +211,12 @@ The SDK provides these routes (same as other SDKs for React provider compatibili
182
211
 
183
212
  - `POST /api/vortex/jwt`
184
213
  - `GET /api/vortex/invitations?targetType=email&targetValue=user@example.com`
185
- - `GET /api/vortex/invitations/:id`
186
- - `DELETE /api/vortex/invitations/:id`
214
+ - `GET /api/vortex/invitations/:invitation_id`
215
+ - `DELETE /api/vortex/invitations/:invitation_id`
187
216
  - `POST /api/vortex/invitations/accept`
188
- - `GET /api/vortex/invitations/by-group/:type/:id`
189
- - `DELETE /api/vortex/invitations/by-group/:type/:id`
190
- - `POST /api/vortex/invitations/:id/reinvite`
217
+ - `GET /api/vortex/invitations/by-scope/:scope_type/:scope`
218
+ - `DELETE /api/vortex/invitations/by-scope/:scope_type/:scope`
219
+ - `POST /api/vortex/invitations/:invitation_id/reinvite`
191
220
  - `POST /api/vortex/invitations/sync-internal-invitation`
192
221
 
193
222
  ## Sync Internal Invitation
@@ -208,16 +237,19 @@ puts "Invitation IDs: #{result['invitationIds']}"
208
237
  ```
209
238
 
210
239
  **Parameters:**
240
+
211
241
  - `creator_id` (String) — The inviter's user ID in your system
212
242
  - `target_value` (String) — The invitee's user ID in your system
213
243
  - `action` ("accepted" | "declined") — The invitation decision
214
244
  - `component_id` (String) — The widget component UUID
215
245
 
216
246
  **Response:**
247
+
217
248
  - `processed` (Integer) — Count of invitations processed
218
249
  - `invitationIds` (Array<String>) — IDs of processed invitations
219
250
 
220
251
  **Use cases:**
252
+
221
253
  - You handle invitation delivery through your own in-app notifications or UI
222
254
  - Users accept/decline invitations within your application
223
255
  - You need to keep Vortex updated with the invitation status
@@ -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,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.9.0'
4
+ VERSION = '1.15.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vortex-ruby-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vortex Software
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-25 00:00:00.000000000 Z
11
+ date: 2026-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday