vortex-ruby-sdk 1.1.0 → 1.1.3
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 +4 -4
- data/.claude/implementation-guide.md +533 -0
- data/CHANGELOG.md +20 -0
- data/PUBLISHING.md +6 -6
- data/lib/vortex/client.rb +74 -4
- data/lib/vortex/types.rb +13 -0
- data/lib/vortex/version.rb +1 -1
- data/lib/vortex.rb +1 -0
- data/vortex-ruby-sdk.gemspec +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2f54f415260bef01b6106dc5d32cf274e4b9c45e00cdd89e18ad6dfbc1df177
|
|
4
|
+
data.tar.gz: 32e1ab2a8a7bc1ee1a3a683d82832a301f4e3403289a64ff8eea4c77410dc104
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 61262106b17af91ce480f7fac66a79a9d4bf2b7b77c264eba92ea07bf7a82967991e3629b59c84f0edbed77054a4f3f9ac661f9a101a4e28f8dd1efc16deafe9
|
|
7
|
+
data.tar.gz: efc03cda77c4d08a66ef056da5d6c72930ce0003bd6418e2cb145a6a95930e0895f7057d4e864c6c78ca7c5a69ac05dca35d0d6e17ea1aec01e480d795244a32
|
|
@@ -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,20 @@
|
|
|
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.1.3] - 2025-01-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **ACCEPT_USER Type**: New preferred format for accepting invitations with `email`, `phone`, and `name` fields
|
|
12
|
+
- Enhanced `accept_invitations` method to support both new User hash format and legacy target format
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **DEPRECATED**: Legacy target hash format for `accept_invitations` - use User hash instead
|
|
16
|
+
- Internal API calls now always use User format for consistency
|
|
17
|
+
- Added warning messages when legacy target format is used
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- 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
|
|
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
|
|
185
|
+
cd sdks/vortex-ruby-sdk
|
|
186
186
|
bundle install
|
|
187
187
|
|
|
188
188
|
- name: Run tests
|
|
189
189
|
run: |
|
|
190
|
-
cd
|
|
190
|
+
cd sdks/vortex-ruby-sdk
|
|
191
191
|
bundle exec rspec
|
|
192
192
|
|
|
193
193
|
- name: Run RuboCop
|
|
194
194
|
run: |
|
|
195
|
-
cd
|
|
195
|
+
cd sdks/vortex-ruby-sdk
|
|
196
196
|
bundle exec rubocop
|
|
197
197
|
|
|
198
198
|
- name: Build gem
|
|
199
199
|
run: |
|
|
200
|
-
cd
|
|
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
|
|
207
|
+
cd sdks/vortex-ruby-sdk
|
|
208
208
|
gem push *.gem
|
|
209
209
|
```
|
|
210
210
|
|
data/lib/vortex/client.rb
CHANGED
|
@@ -146,16 +146,84 @@ module Vortex
|
|
|
146
146
|
raise VortexError, "Failed to revoke invitation: #{e.message}"
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
-
# Accept invitations
|
|
149
|
+
# Accept invitations using the new User format (preferred)
|
|
150
|
+
#
|
|
151
|
+
# Supports three formats:
|
|
152
|
+
# 1. User hash (preferred): { email: '...', phone: '...', name: '...' }
|
|
153
|
+
# 2. Target hash (deprecated): { type: 'email', value: '...' }
|
|
154
|
+
# 3. Array of targets (deprecated): [{ type: 'email', value: '...' }, ...]
|
|
150
155
|
#
|
|
151
156
|
# @param invitation_ids [Array<String>] List of invitation IDs to accept
|
|
152
|
-
# @param
|
|
157
|
+
# @param user_or_target [Hash, Array] User hash with :email/:phone/:name keys, OR legacy target(s)
|
|
153
158
|
# @return [Hash] The accepted invitation result
|
|
154
159
|
# @raise [VortexError] If the request fails
|
|
155
|
-
|
|
160
|
+
#
|
|
161
|
+
# @example New format (preferred)
|
|
162
|
+
# user = { email: 'user@example.com', name: 'John Doe' }
|
|
163
|
+
# result = client.accept_invitations(['inv-123'], user)
|
|
164
|
+
#
|
|
165
|
+
# @example Legacy format (deprecated)
|
|
166
|
+
# target = { type: 'email', value: 'user@example.com' }
|
|
167
|
+
# result = client.accept_invitations(['inv-123'], target)
|
|
168
|
+
def accept_invitations(invitation_ids, user_or_target)
|
|
169
|
+
# Check if it's an array of targets (legacy format with multiple targets)
|
|
170
|
+
if user_or_target.is_a?(Array)
|
|
171
|
+
warn '[Vortex SDK] DEPRECATED: Passing an array of targets is deprecated. ' \
|
|
172
|
+
'Use the User format instead: accept_invitations(invitation_ids, { email: "user@example.com" })'
|
|
173
|
+
|
|
174
|
+
raise VortexError, 'No targets provided' if user_or_target.empty?
|
|
175
|
+
|
|
176
|
+
last_result = nil
|
|
177
|
+
last_exception = nil
|
|
178
|
+
|
|
179
|
+
user_or_target.each do |target|
|
|
180
|
+
begin
|
|
181
|
+
last_result = accept_invitations(invitation_ids, target)
|
|
182
|
+
rescue => e
|
|
183
|
+
last_exception = e
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
raise last_exception if last_exception
|
|
188
|
+
|
|
189
|
+
return last_result || {}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Check if it's a legacy target format (has :type and :value keys)
|
|
193
|
+
is_legacy_target = user_or_target.key?(:type) && user_or_target.key?(:value)
|
|
194
|
+
|
|
195
|
+
if is_legacy_target
|
|
196
|
+
warn '[Vortex SDK] DEPRECATED: Passing a target hash is deprecated. ' \
|
|
197
|
+
'Use the User format instead: accept_invitations(invitation_ids, { email: "user@example.com" })'
|
|
198
|
+
|
|
199
|
+
# Convert target to User format
|
|
200
|
+
target_type = user_or_target[:type]
|
|
201
|
+
target_value = user_or_target[:value]
|
|
202
|
+
|
|
203
|
+
user = {}
|
|
204
|
+
case target_type
|
|
205
|
+
when 'email'
|
|
206
|
+
user[:email] = target_value
|
|
207
|
+
when 'sms', 'phoneNumber'
|
|
208
|
+
user[:phone] = target_value
|
|
209
|
+
else
|
|
210
|
+
# For other types, try to use as email
|
|
211
|
+
user[:email] = target_value
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Recursively call with User format
|
|
215
|
+
return accept_invitations(invitation_ids, user)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# New User format
|
|
219
|
+
user = user_or_target
|
|
220
|
+
|
|
221
|
+
# Validate that either email or phone is provided
|
|
222
|
+
raise VortexError, 'User must have either email or phone' if user[:email].nil? && user[:phone].nil?
|
|
223
|
+
|
|
156
224
|
body = {
|
|
157
225
|
invitationIds: invitation_ids,
|
|
158
|
-
|
|
226
|
+
user: user.compact # Remove nil values
|
|
159
227
|
}
|
|
160
228
|
|
|
161
229
|
response = @connection.post('/api/v1/invitations/accept') do |req|
|
|
@@ -164,6 +232,8 @@ module Vortex
|
|
|
164
232
|
end
|
|
165
233
|
|
|
166
234
|
handle_response(response)
|
|
235
|
+
rescue VortexError
|
|
236
|
+
raise
|
|
167
237
|
rescue => e
|
|
168
238
|
raise VortexError, "Failed to accept invitations: #{e.message}"
|
|
169
239
|
end
|
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
|
# {
|
data/lib/vortex/version.rb
CHANGED
data/lib/vortex.rb
CHANGED
data/vortex-ruby-sdk.gemspec
CHANGED
|
@@ -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.
|
|
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.
|
|
4
|
+
version: 1.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vortex Software
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-01-05 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.
|
|
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
|