vortex-ruby-sdk 1.1.0 → 1.3.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: e7f6c2f423b5aa8099f7ccce759daaf6dcdd114c6052f1ffb9d9f5072d505916
4
- data.tar.gz: 46755e0df9a92907219042a2f2373f6f067aa46f57a1409159cc215ae14823f3
3
+ metadata.gz: ef46095579ff2ce432423b52947fe7b09bcbac70a9aeaaa72c28245f79aee801
4
+ data.tar.gz: b71a185023f2593be2fd5adfa28771eb281de595b8539b023a60448119b29a7b
5
5
  SHA512:
6
- metadata.gz: 55c8a0671bcad40614d75363ef1b0aaf7b2323f47e9dd717940f59fd026b449c64d79b6c7b9bbed6493199c4c3d1beaac97c3e728409407339bb41363c93b1f9
7
- data.tar.gz: fcce54f801ef8115f7567cded83704409fb8436f34056aa296c77210099e0519dd59d8b51f29ff8eb05669acefb088e5cdf4a8a7f2603033f84de1767192b025
6
+ metadata.gz: f9fee1a62aa03ee7184bf596b13644f48bb3df7a6164eec8bae3be10ebe9f66e21c848410831a09ff99136d01a023aa7bebdbbe9cdf95c7333d7a55a94bd58ed
7
+ data.tar.gz: 315cdc79b63d0f79563d25e9568df67043245cd8c3d230ef79e322b02c39c7559eb3f5613f0c575b946b01f651397052e1ca8aa76ec51683bec5988a7b2445d2
@@ -0,0 +1,533 @@
1
+ # Vortex Ruby SDK Implementation Guide
2
+
3
+ **Gem:** `vortex-ruby-sdk`
4
+ **Type:** Base SDK (Core library for Ruby applications)
5
+ **Requires:** Ruby 3.0+
6
+
7
+ ## Prerequisites
8
+ From integration contract you need: API endpoint prefix, scope entity, authentication pattern
9
+ From discovery data you need: Ruby framework (Rails, Sinatra), database ORM, auth pattern
10
+
11
+ ## Key Facts
12
+ - Framework-agnostic Ruby SDK
13
+ - Client-based: instantiate `Vortex::Client` class and call methods
14
+ - Built-in Rails and Sinatra helpers
15
+ - Faraday-based HTTP client
16
+ - Accept invitations requires custom database logic (must implement)
17
+
18
+ ---
19
+
20
+ ## Step 1: Install
21
+
22
+ Add to `Gemfile`:
23
+ ```ruby
24
+ gem 'vortex-ruby-sdk'
25
+ ```
26
+
27
+ Then install:
28
+ ```bash
29
+ bundle install
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Step 2: Set Environment Variable
35
+
36
+ Add to `.env`:
37
+
38
+ ```bash
39
+ VORTEX_API_KEY=VRTX.your-api-key-here.secret
40
+ ```
41
+
42
+ **Rails:** Use `config/credentials.yml.enc` or `dotenv-rails` gem
43
+ **Sinatra:** Use `dotenv` gem
44
+
45
+ **Never commit API key to version control.**
46
+
47
+ ---
48
+
49
+ ## Step 3: Create Vortex Client
50
+
51
+ ### Rails Initializer (`config/initializers/vortex.rb`):
52
+ ```ruby
53
+ Rails.application.config.vortex = Vortex::Client.new(
54
+ Rails.application.credentials.vortex_api_key || ENV['VORTEX_API_KEY']
55
+ )
56
+ ```
57
+
58
+ ### Rails Concern (`app/controllers/concerns/vortex_helper.rb`):
59
+ ```ruby
60
+ module VortexHelper
61
+ extend ActiveSupport::Concern
62
+
63
+ private
64
+
65
+ def vortex_client
66
+ @vortex_client ||= Vortex::Client.new(
67
+ Rails.application.credentials.vortex_api_key || ENV['VORTEX_API_KEY']
68
+ )
69
+ end
70
+ end
71
+ ```
72
+
73
+ ### Sinatra Configuration:
74
+ ```ruby
75
+ # app.rb or config.ru
76
+ require 'sinatra/base'
77
+ require 'vortex'
78
+
79
+ class MyApp < Sinatra::Base
80
+ configure do
81
+ set :vortex_client, Vortex::Client.new(ENV['VORTEX_API_KEY'])
82
+ end
83
+
84
+ helpers do
85
+ def vortex
86
+ settings.vortex_client
87
+ end
88
+ end
89
+ end
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Step 4: Extract Authenticated User
95
+
96
+ ### Rails with Devise:
97
+ ```ruby
98
+ # app/controllers/application_controller.rb
99
+ class ApplicationController < ActionController::API
100
+ private
101
+
102
+ def current_vortex_user
103
+ return nil unless current_user
104
+
105
+ {
106
+ id: current_user.id.to_s,
107
+ email: current_user.email,
108
+ admin_scopes: current_user.admin? ? ['autojoin'] : []
109
+ }
110
+ end
111
+
112
+ def require_authentication!
113
+ render json: { error: 'Unauthorized' }, status: :unauthorized unless current_user
114
+ end
115
+ end
116
+ ```
117
+
118
+ ### Rails with JWT:
119
+ ```ruby
120
+ class ApplicationController < ActionController::API
121
+ before_action :authenticate_user_from_token!
122
+
123
+ private
124
+
125
+ def authenticate_user_from_token!
126
+ token = request.headers['Authorization']&.split(' ')&.last
127
+ return render json: { error: 'Unauthorized' }, status: :unauthorized unless token
128
+
129
+ begin
130
+ payload = JWT.decode(token, Rails.application.credentials.secret_key_base, true, algorithm: 'HS256')[0]
131
+ @current_user = User.find(payload['user_id'])
132
+ rescue JWT::DecodeError, ActiveRecord::RecordNotFound
133
+ render json: { error: 'Unauthorized' }, status: :unauthorized
134
+ end
135
+ end
136
+
137
+ def current_vortex_user
138
+ return nil unless @current_user
139
+
140
+ {
141
+ id: @current_user.id.to_s,
142
+ email: @current_user.email,
143
+ admin_scopes: @current_user.admin? ? ['autojoin'] : []
144
+ }
145
+ end
146
+ end
147
+ ```
148
+
149
+ ### Sinatra:
150
+ ```ruby
151
+ # app/helpers/auth_helper.rb
152
+ module AuthHelper
153
+ def current_user
154
+ return @current_user if defined?(@current_user)
155
+
156
+ # Session-based
157
+ if session[:user_id]
158
+ @current_user = User.find(session[:user_id])
159
+ # JWT-based
160
+ elsif request.env['HTTP_AUTHORIZATION']
161
+ token = request.env['HTTP_AUTHORIZATION'].split(' ').last
162
+ payload = JWT.decode(token, ENV['SECRET_KEY_BASE'], true, algorithm: 'HS256')[0]
163
+ @current_user = User.find(payload['user_id'])
164
+ end
165
+
166
+ @current_user
167
+ rescue
168
+ nil
169
+ end
170
+
171
+ def current_vortex_user
172
+ return nil unless current_user
173
+
174
+ {
175
+ id: current_user.id.to_s,
176
+ email: current_user.email,
177
+ admin_scopes: current_user.admin? ? ['autojoin'] : []
178
+ }
179
+ end
180
+
181
+ def require_authentication!
182
+ halt 401, { error: 'Unauthorized' }.to_json unless current_user
183
+ end
184
+ end
185
+ ```
186
+
187
+ **Adapt to their patterns:**
188
+ - Match their auth mechanism (Devise, JWT, sessions)
189
+ - Match their user structure
190
+ - Match their admin detection logic
191
+
192
+ ---
193
+
194
+ ## Step 5: Implement JWT Generation Endpoint
195
+
196
+ ### Rails (`app/controllers/vortex_controller.rb`):
197
+ ```ruby
198
+ class VortexController < ApplicationController
199
+ before_action :require_authentication!
200
+ include VortexHelper
201
+
202
+ def generate_jwt
203
+ user = current_vortex_user
204
+ extra = params.permit(:componentId, :scope, :scopeType).to_h.compact
205
+
206
+ jwt = vortex_client.generate_jwt(
207
+ user: user,
208
+ attributes: extra.empty? ? nil : extra
209
+ )
210
+
211
+ render json: { jwt: jwt }
212
+ rescue Vortex::VortexError => e
213
+ Rails.logger.error("JWT generation error: #{e.message}")
214
+ render json: { error: 'Internal server error' }, status: :internal_server_error
215
+ end
216
+ end
217
+ ```
218
+
219
+ ### Sinatra:
220
+ ```ruby
221
+ require 'sinatra/base'
222
+ require 'json'
223
+
224
+ class MyApp < Sinatra::Base
225
+ helpers AuthHelper
226
+
227
+ post '/api/vortex/jwt' do
228
+ require_authentication!
229
+
230
+ content_type :json
231
+
232
+ begin
233
+ user = current_vortex_user
234
+ request_body = JSON.parse(request.body.read) rescue {}
235
+
236
+ extra = request_body.slice('componentId', 'scope', 'scopeType').compact
237
+ extra = nil if extra.empty?
238
+
239
+ jwt = vortex.generate_jwt(
240
+ user: user,
241
+ attributes: extra
242
+ )
243
+
244
+ { jwt: jwt }.to_json
245
+ rescue Vortex::VortexError => e
246
+ logger.error("JWT generation error: #{e.message}")
247
+ status 500
248
+ { error: 'Internal server error' }.to_json
249
+ end
250
+ end
251
+ end
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Step 6: Implement Accept Invitations Endpoint (CRITICAL)
257
+
258
+ ### Rails Routes (`config/routes.rb`):
259
+ ```ruby
260
+ Rails.application.routes.draw do
261
+ namespace :api do
262
+ namespace :vortex do
263
+ post 'jwt', to: 'vortex#generate_jwt'
264
+ get 'invitations', to: 'vortex#get_invitations_by_target'
265
+ get 'invitations/:invitation_id', to: 'vortex#get_invitation'
266
+ post 'invitations/accept', to: 'vortex#accept_invitations'
267
+ delete 'invitations/:invitation_id', to: 'vortex#revoke_invitation'
268
+ post 'invitations/:invitation_id/reinvite', to: 'vortex#reinvite'
269
+ end
270
+ end
271
+ end
272
+ ```
273
+
274
+ ### Rails with ActiveRecord:
275
+ ```ruby
276
+ class Api::VortexController < ApplicationController
277
+ before_action :require_authentication!
278
+ include VortexHelper
279
+
280
+ def accept_invitations
281
+ invitation_ids = params[:invitationIds] || []
282
+ user = params[:user]
283
+
284
+ return render json: { error: 'Missing invitationIds or user' }, status: :bad_request if invitation_ids.empty? || !user
285
+
286
+ begin
287
+ # 1. Mark as accepted in Vortex
288
+ vortex_client.accept_invitations(invitation_ids, user)
289
+
290
+ # 2. CRITICAL - Add to database
291
+ ActiveRecord::Base.transaction do
292
+ invitation_ids.each do |invitation_id|
293
+ invitation = vortex_client.get_invitation(invitation_id)
294
+
295
+ (invitation['groups'] || []).each do |group|
296
+ GroupMembership.find_or_create_by!(
297
+ user_id: current_user.id,
298
+ group_type: group['type'],
299
+ group_id: group['groupId']
300
+ ) do |membership|
301
+ membership.role = invitation['role'] || 'member'
302
+ end
303
+ end
304
+ end
305
+ end
306
+
307
+ render json: {
308
+ success: true,
309
+ acceptedCount: invitation_ids.length
310
+ }
311
+ rescue Vortex::VortexError => e
312
+ Rails.logger.error("Accept invitations error: #{e.message}")
313
+ render json: { error: 'Internal server error' }, status: :internal_server_error
314
+ rescue => e
315
+ Rails.logger.error("Database error: #{e.message}")
316
+ render json: { error: 'Internal server error' }, status: :internal_server_error
317
+ end
318
+ end
319
+ end
320
+ ```
321
+
322
+ ### Sinatra with Sequel:
323
+ ```ruby
324
+ post '/api/vortex/invitations/accept' do
325
+ require_authentication!
326
+ content_type :json
327
+
328
+ request_body = JSON.parse(request.body.read)
329
+ invitation_ids = request_body['invitationIds'] || []
330
+ user = request_body['user']
331
+
332
+ halt 400, { error: 'Missing invitationIds or user' }.to_json if invitation_ids.empty? || !user
333
+
334
+ begin
335
+ # 1. Mark as accepted in Vortex
336
+ vortex.accept_invitations(invitation_ids, user)
337
+
338
+ # 2. CRITICAL - Add to database
339
+ DB.transaction do
340
+ invitation_ids.each do |invitation_id|
341
+ invitation = vortex.get_invitation(invitation_id)
342
+
343
+ (invitation['groups'] || []).each do |group|
344
+ GroupMembership.insert_conflict(
345
+ target: [:user_id, :group_type, :group_id],
346
+ update: { role: invitation['role'] || 'member' }
347
+ ).insert(
348
+ user_id: current_user.id,
349
+ group_type: group['type'],
350
+ group_id: group['groupId'],
351
+ role: invitation['role'] || 'member',
352
+ joined_at: Time.now
353
+ )
354
+ end
355
+ end
356
+ end
357
+
358
+ {
359
+ success: true,
360
+ acceptedCount: invitation_ids.length
361
+ }.to_json
362
+ rescue Vortex::VortexError => e
363
+ logger.error("Accept invitations error: #{e.message}")
364
+ status 500
365
+ { error: 'Internal server error' }.to_json
366
+ end
367
+ end
368
+ ```
369
+
370
+ **Critical - Adapt database logic:**
371
+ - Use their actual table/model names (from discovery)
372
+ - Use their actual field names
373
+ - Use their ORM pattern (ActiveRecord, Sequel)
374
+ - Handle duplicate memberships if needed
375
+
376
+ ---
377
+
378
+ ## Step 7: Database Models
379
+
380
+ ### Rails Migration:
381
+ ```ruby
382
+ # db/migrate/YYYYMMDDHHMMSS_create_group_memberships.rb
383
+ class CreateGroupMemberships < ActiveRecord::Migration[7.0]
384
+ def change
385
+ create_table :group_memberships do |t|
386
+ t.string :user_id, null: false
387
+ t.string :group_type, null: false, limit: 100
388
+ t.string :group_id, null: false
389
+ t.string :role, default: 'member', limit: 50
390
+ t.timestamp :joined_at, null: false, default: -> { 'CURRENT_TIMESTAMP' }
391
+
392
+ t.index [:user_id, :group_type, :group_id], unique: true, name: 'unique_membership'
393
+ t.index [:group_type, :group_id], name: 'idx_group'
394
+ t.index [:user_id], name: 'idx_user'
395
+ end
396
+ end
397
+ end
398
+ ```
399
+
400
+ ### Rails Model:
401
+ ```ruby
402
+ # app/models/group_membership.rb
403
+ class GroupMembership < ApplicationRecord
404
+ validates :user_id, presence: true
405
+ validates :group_type, presence: true
406
+ validates :group_id, presence: true
407
+ validates :role, presence: true
408
+
409
+ validates :user_id, uniqueness: { scope: [:group_type, :group_id] }
410
+ end
411
+ ```
412
+
413
+ ### Sequel Migration:
414
+ ```ruby
415
+ # db/migrations/001_create_group_memberships.rb
416
+ Sequel.migration do
417
+ change do
418
+ create_table(:group_memberships) do
419
+ primary_key :id
420
+ String :user_id, null: false
421
+ String :group_type, size: 100, null: false
422
+ String :group_id, null: false
423
+ String :role, size: 50, default: 'member'
424
+ DateTime :joined_at, null: false, default: Sequel::CURRENT_TIMESTAMP
425
+
426
+ index [:user_id, :group_type, :group_id], unique: true, name: :unique_membership
427
+ index [:group_type, :group_id], name: :idx_group
428
+ index [:user_id], name: :idx_user
429
+ end
430
+ end
431
+ end
432
+ ```
433
+
434
+ ---
435
+
436
+ ## Step 8: Build and Test
437
+
438
+ ```bash
439
+ # Run migrations
440
+ rails db:migrate # Rails
441
+ sequel -m db/migrations $DATABASE_URL # Sequel
442
+
443
+ # Start server
444
+ rails server # Rails
445
+ bundle exec rackup # Sinatra
446
+
447
+ # Test JWT endpoint
448
+ curl -X POST http://localhost:3000/api/vortex/jwt \
449
+ -H "Authorization: Bearer your-auth-token"
450
+ ```
451
+
452
+ Expected response:
453
+ ```json
454
+ {
455
+ "jwt": "eyJhbGciOiJIUzI1NiIs..."
456
+ }
457
+ ```
458
+
459
+ ---
460
+
461
+ ## Common Errors
462
+
463
+ **"LoadError: cannot load such file -- vortex"** → Run `bundle install`
464
+
465
+ **"VORTEX_API_KEY not set"** → Check `.env` file, credentials, or environment variables
466
+
467
+ **User not added to database** → Must implement database logic in accept handler (see Step 6)
468
+
469
+ **"NoMethodError: undefined method `admin?'"** → Implement admin check in User model
470
+
471
+ **CORS errors** → Add CORS middleware:
472
+
473
+ **Rails:**
474
+ ```ruby
475
+ # Gemfile
476
+ gem 'rack-cors'
477
+
478
+ # config/initializers/cors.rb
479
+ Rails.application.config.middleware.insert_before 0, Rack::Cors do
480
+ allow do
481
+ origins 'http://localhost:3000'
482
+ resource '*', headers: :any, methods: [:get, :post, :delete, :options]
483
+ end
484
+ end
485
+ ```
486
+
487
+ **Sinatra:**
488
+ ```ruby
489
+ require 'sinatra/cross_origin'
490
+
491
+ class MyApp < Sinatra::Base
492
+ register Sinatra::CrossOrigin
493
+
494
+ configure do
495
+ enable :cross_origin
496
+ end
497
+
498
+ options '*' do
499
+ response.headers['Access-Control-Allow-Methods'] = 'GET, POST, DELETE, OPTIONS'
500
+ response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
501
+ 200
502
+ end
503
+ end
504
+ ```
505
+
506
+ ---
507
+
508
+ ## After Implementation Report
509
+
510
+ List files created/modified:
511
+ - Dependency: Gemfile
512
+ - Client: config/initializers/vortex.rb (or concern)
513
+ - 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
516
+
517
+ Confirm:
518
+ - Vortex gem installed
519
+ - VortexClient instance created
520
+ - JWT endpoint returns valid JWT
521
+ - Accept invitations includes database logic
522
+ - Routes registered at correct prefix
523
+ - Migrations run
524
+
525
+ ## Endpoints Registered
526
+
527
+ All endpoints at `/api/vortex`:
528
+ - `POST /jwt` - Generate JWT for authenticated user
529
+ - `GET /invitations` - Get invitations by target
530
+ - `GET /invitations/:id` - Get invitation by ID
531
+ - `POST /invitations/accept` - Accept invitations (custom DB logic)
532
+ - `DELETE /invitations/:id` - Revoke invitation
533
+ - `POST /invitations/:id/reinvite` - Resend invitation
data/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to the Vortex Ruby SDK will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.2.0] - 2026-01-23
9
+
10
+ ### Added
11
+ - **Internal Invitations**: New `'internal'` delivery type for customer-managed invitations
12
+ - Support for `deliveryTypes: ['internal']`
13
+ - No email/SMS communication triggered by Vortex
14
+ - Target value can be any customer-defined identifier
15
+ - Useful for in-app invitation flows managed by customer's application
16
+
17
+ ### Changed
18
+ - Updated `deliveryTypes` field documentation to include `'internal'` as a valid value
19
+
20
+ ## [1.1.3] - 2025-01-29
21
+
22
+ ### Added
23
+ - **ACCEPT_USER Type**: New preferred format for accepting invitations with `email`, `phone`, and `name` fields
24
+ - Enhanced `accept_invitations` method to support both new User hash format and legacy target format
25
+
26
+ ### Changed
27
+ - **DEPRECATED**: Legacy target hash format for `accept_invitations` - use User hash instead
28
+ - Internal API calls now always use User format for consistency
29
+ - Added warning messages when legacy target format is used
30
+
31
+ ### Fixed
32
+ - Maintained 100% backward compatibility - existing code using legacy target format continues to work
data/PUBLISHING.md CHANGED
@@ -76,7 +76,7 @@ Create or update `CHANGELOG.md` with release notes:
76
76
  ### Step 4: Install Dependencies and Test
77
77
 
78
78
  ```bash
79
- cd packages/vortex-ruby-sdk
79
+ cd sdks/vortex-ruby-sdk
80
80
 
81
81
  # Install dependencies
82
82
  bundle install
@@ -182,29 +182,29 @@ jobs:
182
182
 
183
183
  - name: Install dependencies
184
184
  run: |
185
- cd packages/vortex-ruby-sdk
185
+ cd sdks/vortex-ruby-sdk
186
186
  bundle install
187
187
 
188
188
  - name: Run tests
189
189
  run: |
190
- cd packages/vortex-ruby-sdk
190
+ cd sdks/vortex-ruby-sdk
191
191
  bundle exec rspec
192
192
 
193
193
  - name: Run RuboCop
194
194
  run: |
195
- cd packages/vortex-ruby-sdk
195
+ cd sdks/vortex-ruby-sdk
196
196
  bundle exec rubocop
197
197
 
198
198
  - name: Build gem
199
199
  run: |
200
- cd packages/vortex-ruby-sdk
200
+ cd sdks/vortex-ruby-sdk
201
201
  gem build vortex-ruby-sdk.gemspec
202
202
 
203
203
  - name: Publish to RubyGems
204
204
  env:
205
205
  GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
206
206
  run: |
207
- cd packages/vortex-ruby-sdk
207
+ cd sdks/vortex-ruby-sdk
208
208
  gem push *.gem
209
209
  ```
210
210
 
data/README.md CHANGED
@@ -12,6 +12,8 @@ A Ruby SDK for the Vortex invitation system, providing seamless integration with
12
12
  - **Same Route Structure**: Ensures React provider compatibility
13
13
  - **Comprehensive Testing**: Full test coverage with RSpec
14
14
  - **Type Safety**: Clear method signatures and documentation
15
+ - **Multiple Delivery Types**: Support for `email`, `phone`, `share`, and `internal` invitation delivery
16
+ - `internal` invitations allow for customer-managed, in-app invitation flows with no external communication
15
17
 
16
18
  ## Installation
17
19
 
data/lib/vortex/client.rb CHANGED
@@ -80,6 +80,16 @@ module Vortex
80
80
  expires: expires
81
81
  }
82
82
 
83
+ # Add name if present (convert snake_case to camelCase for JWT)
84
+ if user[:name]
85
+ payload[:name] = user[:name]
86
+ end
87
+
88
+ # Add avatarUrl if present (convert snake_case to camelCase for JWT)
89
+ if user[:avatar_url]
90
+ payload[:avatarUrl] = user[:avatar_url]
91
+ end
92
+
83
93
  # Add adminScopes if present
84
94
  if user[:admin_scopes]
85
95
  payload[:adminScopes] = user[:admin_scopes]
@@ -146,16 +156,84 @@ module Vortex
146
156
  raise VortexError, "Failed to revoke invitation: #{e.message}"
147
157
  end
148
158
 
149
- # Accept invitations
159
+ # Accept invitations using the new User format (preferred)
160
+ #
161
+ # Supports three formats:
162
+ # 1. User hash (preferred): { email: '...', phone: '...', name: '...' }
163
+ # 2. Target hash (deprecated): { type: 'email', value: '...' }
164
+ # 3. Array of targets (deprecated): [{ type: 'email', value: '...' }, ...]
150
165
  #
151
166
  # @param invitation_ids [Array<String>] List of invitation IDs to accept
152
- # @param target [Hash] Target hash with :type and :value
167
+ # @param user_or_target [Hash, Array] User hash with :email/:phone/:name keys, OR legacy target(s)
153
168
  # @return [Hash] The accepted invitation result
154
169
  # @raise [VortexError] If the request fails
155
- def accept_invitations(invitation_ids, target)
170
+ #
171
+ # @example New format (preferred)
172
+ # user = { email: 'user@example.com', name: 'John Doe' }
173
+ # result = client.accept_invitations(['inv-123'], user)
174
+ #
175
+ # @example Legacy format (deprecated)
176
+ # target = { type: 'email', value: 'user@example.com' }
177
+ # result = client.accept_invitations(['inv-123'], target)
178
+ def accept_invitations(invitation_ids, user_or_target)
179
+ # Check if it's an array of targets (legacy format with multiple targets)
180
+ if user_or_target.is_a?(Array)
181
+ warn '[Vortex SDK] DEPRECATED: Passing an array of targets is deprecated. ' \
182
+ 'Use the User format instead: accept_invitations(invitation_ids, { email: "user@example.com" })'
183
+
184
+ raise VortexError, 'No targets provided' if user_or_target.empty?
185
+
186
+ last_result = nil
187
+ last_exception = nil
188
+
189
+ user_or_target.each do |target|
190
+ begin
191
+ last_result = accept_invitations(invitation_ids, target)
192
+ rescue => e
193
+ last_exception = e
194
+ end
195
+ end
196
+
197
+ raise last_exception if last_exception
198
+
199
+ return last_result || {}
200
+ end
201
+
202
+ # Check if it's a legacy target format (has :type and :value keys)
203
+ is_legacy_target = user_or_target.key?(:type) && user_or_target.key?(:value)
204
+
205
+ if is_legacy_target
206
+ warn '[Vortex SDK] DEPRECATED: Passing a target hash is deprecated. ' \
207
+ 'Use the User format instead: accept_invitations(invitation_ids, { email: "user@example.com" })'
208
+
209
+ # Convert target to User format
210
+ target_type = user_or_target[:type]
211
+ target_value = user_or_target[:value]
212
+
213
+ user = {}
214
+ case target_type
215
+ when 'email'
216
+ user[:email] = target_value
217
+ when 'phone', 'phoneNumber'
218
+ user[:phone] = target_value
219
+ else
220
+ # For other types, try to use as email
221
+ user[:email] = target_value
222
+ end
223
+
224
+ # Recursively call with User format
225
+ return accept_invitations(invitation_ids, user)
226
+ end
227
+
228
+ # New User format
229
+ user = user_or_target
230
+
231
+ # Validate that either email or phone is provided
232
+ raise VortexError, 'User must have either email or phone' if user[:email].nil? && user[:phone].nil?
233
+
156
234
  body = {
157
235
  invitationIds: invitation_ids,
158
- target: target
236
+ user: user.compact # Remove nil values
159
237
  }
160
238
 
161
239
  response = @connection.post('/api/v1/invitations/accept') do |req|
@@ -164,6 +242,8 @@ module Vortex
164
242
  end
165
243
 
166
244
  handle_response(response)
245
+ rescue VortexError
246
+ raise
167
247
  rescue => e
168
248
  raise VortexError, "Failed to accept invitations: #{e.message}"
169
249
  end
@@ -207,6 +287,86 @@ module Vortex
207
287
  raise VortexError, "Failed to reinvite: #{e.message}"
208
288
  end
209
289
 
290
+ # Create an invitation from your backend
291
+ #
292
+ # This method allows you to create invitations programmatically using your API key,
293
+ # without requiring a user JWT token. Useful for server-side invitation creation,
294
+ # such as "People You May Know" flows or admin-initiated invitations.
295
+ #
296
+ # Target types:
297
+ # - 'email': Send an email invitation
298
+ # - 'phone': Create a phone invitation (short link returned for you to send)
299
+ # - 'internal': Create an internal invitation for PYMK flows (no email sent)
300
+ #
301
+ # @param widget_configuration_id [String] The widget configuration ID to use
302
+ # @param target [Hash] The invitation target: { type: 'email|sms|internal', value: '...' }
303
+ # @param inviter [Hash] The inviter info: { user_id: '...', user_email: '...', name: '...' }
304
+ # @param groups [Array<Hash>, nil] Optional groups: [{ type: '...', group_id: '...', name: '...' }]
305
+ # @param source [String, nil] Optional source for analytics (defaults to 'api')
306
+ # @param template_variables [Hash, nil] Optional template variables for email customization
307
+ # @param metadata [Hash, nil] Optional metadata passed through to webhooks
308
+ # @return [Hash] Created invitation with :id, :short_link, :status, :created_at
309
+ # @raise [VortexError] If the request fails
310
+ #
311
+ # @example Create an email invitation
312
+ # result = client.create_invitation(
313
+ # 'widget-config-123',
314
+ # { type: 'email', value: 'invitee@example.com' },
315
+ # { user_id: 'user-456', user_email: 'inviter@example.com', name: 'John Doe' },
316
+ # [{ type: 'team', group_id: 'team-789', name: 'Engineering' }]
317
+ # )
318
+ #
319
+ # @example Create an internal invitation (PYMK flow - no email sent)
320
+ # result = client.create_invitation(
321
+ # 'widget-config-123',
322
+ # { type: 'internal', value: 'internal-user-abc' },
323
+ # { user_id: 'user-456' },
324
+ # nil,
325
+ # 'pymk'
326
+ # )
327
+ def create_invitation(widget_configuration_id, target, inviter, groups = nil, source = nil, template_variables = nil, metadata = nil)
328
+ raise VortexError, 'widget_configuration_id is required' if widget_configuration_id.nil? || widget_configuration_id.empty?
329
+ raise VortexError, 'target must have type and value' if target[:type].nil? || target[:value].nil?
330
+ raise VortexError, 'inviter must have user_id' if inviter[:user_id].nil?
331
+
332
+ # Build request body with camelCase keys for the API
333
+ body = {
334
+ widgetConfigurationId: widget_configuration_id,
335
+ target: target,
336
+ inviter: {
337
+ userId: inviter[:user_id],
338
+ userEmail: inviter[:user_email],
339
+ name: inviter[:name],
340
+ avatarUrl: inviter[:avatar_url]
341
+ }.compact
342
+ }
343
+
344
+ if groups && !groups.empty?
345
+ body[:groups] = groups.map do |g|
346
+ {
347
+ type: g[:type],
348
+ groupId: g[:group_id],
349
+ name: g[:name]
350
+ }
351
+ end
352
+ end
353
+
354
+ body[:source] = source if source
355
+ body[:templateVariables] = template_variables if template_variables
356
+ body[:metadata] = metadata if metadata
357
+
358
+ response = @connection.post('/api/v1/invitations') do |req|
359
+ req.headers['Content-Type'] = 'application/json'
360
+ req.body = JSON.generate(body)
361
+ end
362
+
363
+ handle_response(response)
364
+ rescue VortexError
365
+ raise
366
+ rescue => e
367
+ raise VortexError, "Failed to create invitation: #{e.message}"
368
+ end
369
+
210
370
  private
211
371
 
212
372
  def build_connection
data/lib/vortex/types.rb CHANGED
@@ -39,6 +39,19 @@ module Vortex
39
39
  createdAt: String # ISO 8601 timestamp when the group was created
40
40
  }.freeze
41
41
 
42
+ # AcceptUser structure for accepting invitations (new format - preferred)
43
+ # @example
44
+ # {
45
+ # email: 'user@example.com',
46
+ # phone: '+1234567890', # Optional
47
+ # name: 'John Doe' # Optional
48
+ # }
49
+ 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
53
+ }.freeze
54
+
42
55
  # Invitation structure from API responses
43
56
  # @example
44
57
  # {
@@ -56,7 +69,7 @@ module Vortex
56
69
  createdAt: String,
57
70
  deactivated: :boolean,
58
71
  deliveryCount: Integer,
59
- deliveryTypes: Array, # of String
72
+ deliveryTypes: Array, # of String - valid values: "email", "phone", "share", "internal"
60
73
  foreignCreatorId: String,
61
74
  invitationType: String,
62
75
  modifiedAt: String,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vortex
4
- VERSION = '1.1.0'
4
+ VERSION = '1.3.0'
5
5
  end
data/lib/vortex.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'vortex/version'
4
4
  require 'vortex/error'
5
+ require 'vortex/types'
5
6
  require 'vortex/client'
6
7
 
7
8
  # Vortex Ruby SDK
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
6
6
  spec.name = 'vortex-ruby-sdk'
7
7
  spec.version = Vortex::VERSION
8
8
  spec.authors = ['Vortex Software']
9
- spec.email = ['support@vortexsoftware.io']
9
+ spec.email = ['support@vortexsoftware.com']
10
10
 
11
11
  spec.summary = 'Ruby SDK for Vortex invitation system'
12
12
  spec.description = 'A Ruby SDK that provides seamless integration with the Vortex invitation system, including JWT generation and invitation management.'
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.1.0
4
+ version: 1.3.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: 2025-12-16 00:00:00.000000000 Z
11
+ date: 2026-01-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -139,11 +139,13 @@ dependencies:
139
139
  description: A Ruby SDK that provides seamless integration with the Vortex invitation
140
140
  system, including JWT generation and invitation management.
141
141
  email:
142
- - support@vortexsoftware.io
142
+ - support@vortexsoftware.com
143
143
  executables: []
144
144
  extensions: []
145
145
  extra_rdoc_files: []
146
146
  files:
147
+ - ".claude/implementation-guide.md"
148
+ - CHANGELOG.md
147
149
  - LICENSE
148
150
  - PUBLISHING.md
149
151
  - README.md