workos 5.10.0 → 5.11.1
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/Gemfile.lock +2 -2
- data/lib/workos/cache.rb +94 -0
- data/lib/workos/session.rb +6 -5
- data/lib/workos/user_management.rb +6 -2
- data/lib/workos/version.rb +1 -1
- data/lib/workos.rb +1 -0
- data/spec/lib/workos/cache_spec.rb +94 -0
- data/spec/lib/workos/session_spec.rb +76 -12
- data/spec/lib/workos/user_management_spec.rb +21 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2792dbbd0e3dac4d2a9eac0920a135b0db63c723a85fbbec049327ab2cffd547
|
4
|
+
data.tar.gz: 356cb856f6e2df599daf2affc94eb74d4ee885ab71d77e6002ab176718dbc0ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d4a1406d88ac981d0d0653027cf2afe17b64d7c577d6cfe0d4ccc9ffb0a6c6be29c0f3ed140a03bb8ce5a8876990e41bea60a892f5675dc6578ce1526a4504f6
|
7
|
+
data.tar.gz: 55abb8c32270ce7b4b5e4e7142f9f6c390e9cca88c3f3eb1242840193f4155da9bf4f018d85e5b34f51ebd3cc85f0cac176c8cd00abffd077133962100083566
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
workos (5.
|
4
|
+
workos (5.11.1)
|
5
5
|
encryptor (~> 3.0)
|
6
6
|
jwt (~> 2.8)
|
7
7
|
|
@@ -19,7 +19,7 @@ GEM
|
|
19
19
|
diff-lcs (1.5.1)
|
20
20
|
encryptor (3.0.0)
|
21
21
|
hashdiff (1.1.0)
|
22
|
-
jwt (2.
|
22
|
+
jwt (2.10.1)
|
23
23
|
base64
|
24
24
|
parallel (1.24.0)
|
25
25
|
parser (3.3.0.5)
|
data/lib/workos/cache.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WorkOS
|
4
|
+
# The Cache module provides a simple in-memory cache for storing values
|
5
|
+
# This module is not meant to be instantiated in a user space, and is used internally by the SDK
|
6
|
+
module Cache
|
7
|
+
# The Entry class represents a cache entry with a value and an expiration time
|
8
|
+
class Entry
|
9
|
+
attr_reader :value, :expires_at
|
10
|
+
|
11
|
+
# Initializes a new cache entry
|
12
|
+
# @param value [Object] The value to store in the cache
|
13
|
+
# @param expires_in_seconds [Integer, nil] The expiration time for the value in seconds, or nil for no expiration
|
14
|
+
def initialize(value, expires_in_seconds)
|
15
|
+
@value = value
|
16
|
+
@expires_at = expires_in_seconds ? Time.now + expires_in_seconds : nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Checks if the entry has expired
|
20
|
+
# @return [Boolean] True if the entry has expired, false otherwise
|
21
|
+
def expired?
|
22
|
+
return false if expires_at.nil?
|
23
|
+
|
24
|
+
Time.now > @expires_at
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
# Fetches a value from the cache, or calls the block to fetch the value if it is not present
|
30
|
+
# @param key [String] The key to fetch the value for
|
31
|
+
# @param expires_in [Integer] The expiration time for the value in seconds
|
32
|
+
# @param force [Boolean] If true, the value will be fetched from the block even if it is present in the cache
|
33
|
+
# @param block [Proc] The block to call to fetch the value if it is not present in the cache
|
34
|
+
# @return [Object] The value fetched from the cache or the block
|
35
|
+
def fetch(key, expires_in: nil, force: false, &block)
|
36
|
+
entry = store[key]
|
37
|
+
|
38
|
+
if force || entry.nil? || entry.expired?
|
39
|
+
value = block.call
|
40
|
+
store[key] = Entry.new(value, expires_in)
|
41
|
+
return value
|
42
|
+
end
|
43
|
+
|
44
|
+
entry.value
|
45
|
+
end
|
46
|
+
|
47
|
+
# Reads a value from the cache
|
48
|
+
# @param key [String] The key to read the value for
|
49
|
+
# @return [Object] The value read from the cache, or nil if the value is not present or has expired
|
50
|
+
def read(key)
|
51
|
+
entry = store[key]
|
52
|
+
return nil if entry.nil? || entry.expired?
|
53
|
+
|
54
|
+
entry.value
|
55
|
+
end
|
56
|
+
|
57
|
+
# Writes a value to the cache
|
58
|
+
# @param key [String] The key to write the value for
|
59
|
+
# @param value [Object] The value to write to the cache
|
60
|
+
# @param expires_in [Integer] The expiration time for the value in seconds
|
61
|
+
# @return [Object] The value written to the cache
|
62
|
+
def write(key, value, expires_in: nil)
|
63
|
+
store[key] = Entry.new(value, expires_in)
|
64
|
+
value
|
65
|
+
end
|
66
|
+
|
67
|
+
# Deletes a value from the cache
|
68
|
+
# @param key [String] The key to delete the value for
|
69
|
+
def delete(key)
|
70
|
+
store.delete(key)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Clears all values from the cache
|
74
|
+
def clear
|
75
|
+
store.clear
|
76
|
+
end
|
77
|
+
|
78
|
+
# Checks if a value exists in the cache
|
79
|
+
# @param key [String] The key to check for
|
80
|
+
# @return [Boolean] True if the value exists and has not expired, false otherwise
|
81
|
+
def exist?(key)
|
82
|
+
entry = store[key]
|
83
|
+
!(entry.nil? || entry.expired?)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# The in-memory store for the cache
|
89
|
+
def store
|
90
|
+
@store ||= {}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/workos/session.rb
CHANGED
@@ -23,7 +23,9 @@ module WorkOS
|
|
23
23
|
@session_data = session_data
|
24
24
|
@client_id = client_id
|
25
25
|
|
26
|
-
@jwks =
|
26
|
+
@jwks = Cache.fetch("jwks_#{client_id}", expires_in: 5 * 60) do
|
27
|
+
create_remote_jwk_set(URI(@user_management.get_jwks_url(client_id)))
|
28
|
+
end
|
27
29
|
@jwks_algorithms = @jwks.map { |key| key[:alg] }.compact.uniq
|
28
30
|
end
|
29
31
|
|
@@ -101,18 +103,17 @@ module WorkOS
|
|
101
103
|
# rubocop:enable Metrics/PerceivedComplexity
|
102
104
|
|
103
105
|
# Returns a URL to redirect the user to for logging out
|
106
|
+
# @param return_to [String] The URL to redirect the user to after logging out
|
104
107
|
# @return [String] The URL to redirect the user to for logging out
|
105
|
-
|
106
|
-
def get_logout_url
|
108
|
+
def get_logout_url(return_to: nil)
|
107
109
|
auth_response = authenticate
|
108
110
|
|
109
111
|
unless auth_response[:authenticated]
|
110
112
|
raise "Failed to extract session ID for logout URL: #{auth_response[:reason]}"
|
111
113
|
end
|
112
114
|
|
113
|
-
@user_management.get_logout_url(session_id: auth_response[:session_id])
|
115
|
+
@user_management.get_logout_url(session_id: auth_response[:session_id], return_to: return_to)
|
114
116
|
end
|
115
|
-
# rubocop:enable Naming/AccessorMethodName
|
116
117
|
|
117
118
|
# Encrypts and seals data using AES-256-GCM
|
118
119
|
# @param data [Hash] The data to seal
|
@@ -530,13 +530,17 @@ module WorkOS
|
|
530
530
|
#
|
531
531
|
# @param [String] session_id The session ID can be found in the `sid`
|
532
532
|
# claim of the access token
|
533
|
+
# @param [String] return_to The URL to redirect the user to after logging out
|
533
534
|
#
|
534
535
|
# @return String
|
535
|
-
def get_logout_url(session_id:)
|
536
|
+
def get_logout_url(session_id:, return_to: nil)
|
537
|
+
params = { session_id: session_id }
|
538
|
+
params[:return_to] = return_to if return_to
|
539
|
+
|
536
540
|
URI::HTTPS.build(
|
537
541
|
host: WorkOS.config.api_hostname,
|
538
542
|
path: '/user_management/sessions/logout',
|
539
|
-
query:
|
543
|
+
query: URI.encode_www_form(params),
|
540
544
|
).to_s
|
541
545
|
end
|
542
546
|
|
data/lib/workos/version.rb
CHANGED
data/lib/workos.rb
CHANGED
@@ -45,6 +45,7 @@ module WorkOS
|
|
45
45
|
autoload :AuthenticationFactorAndChallenge, 'workos/authentication_factor_and_challenge'
|
46
46
|
autoload :AuthenticationResponse, 'workos/authentication_response'
|
47
47
|
autoload :AuditLogs, 'workos/audit_logs'
|
48
|
+
autoload :Cache, 'workos/cache'
|
48
49
|
autoload :Challenge, 'workos/challenge'
|
49
50
|
autoload :Client, 'workos/client'
|
50
51
|
autoload :Connection, 'workos/connection'
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe WorkOS::Cache do
|
4
|
+
before { described_class.clear }
|
5
|
+
|
6
|
+
describe '.write and .read' do
|
7
|
+
it 'stores and retrieves data' do
|
8
|
+
described_class.write('key', 'value')
|
9
|
+
expect(described_class.read('key')).to eq('value')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'returns nil if key does not exist' do
|
13
|
+
expect(described_class.read('missing')).to be_nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '.fetch' do
|
18
|
+
it 'returns cached value when present and not expired' do
|
19
|
+
described_class.write('key', 'value')
|
20
|
+
fetch_value = described_class.fetch('key') { 'new_value' }
|
21
|
+
expect(fetch_value).to eq('value')
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'executes block and caches value when not present' do
|
25
|
+
fetch_value = described_class.fetch('key') { 'new_value' }
|
26
|
+
expect(fetch_value).to eq('new_value')
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'executes block and caches value when force is true' do
|
30
|
+
described_class.write('key', 'value')
|
31
|
+
fetch_value = described_class.fetch('key', force: true) { 'new_value' }
|
32
|
+
expect(fetch_value).to eq('new_value')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'expiration' do
|
37
|
+
it 'expires values after specified time' do
|
38
|
+
described_class.write('key', 'value', expires_in: 0.1)
|
39
|
+
expect(described_class.read('key')).to eq('value')
|
40
|
+
sleep 0.2
|
41
|
+
expect(described_class.read('key')).to be_nil
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'executes block and caches new value when expired' do
|
45
|
+
described_class.write('key', 'old_value', expires_in: 0.1)
|
46
|
+
sleep 0.2
|
47
|
+
fetch_value = described_class.fetch('key') { 'new_value' }
|
48
|
+
expect(fetch_value).to eq('new_value')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'does not expire values when expires_in is nil' do
|
52
|
+
described_class.write('key', 'value', expires_in: nil)
|
53
|
+
sleep 0.2
|
54
|
+
expect(described_class.read('key')).to eq('value')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '.exist?' do
|
59
|
+
it 'returns true if key exists' do
|
60
|
+
described_class.write('key', 'value')
|
61
|
+
expect(described_class.exist?('key')).to be true
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'returns false if expired' do
|
65
|
+
described_class.write('key', 'value', expires_in: 0.1)
|
66
|
+
sleep 0.2
|
67
|
+
expect(described_class.exist?('key')).to be false
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'returns false if key does not exist' do
|
71
|
+
expect(described_class.exist?('missing')).to be false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe '.delete' do
|
76
|
+
it 'deletes key' do
|
77
|
+
described_class.write('key', 'value')
|
78
|
+
described_class.delete('key')
|
79
|
+
expect(described_class.read('key')).to be_nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '.clear' do
|
84
|
+
it 'removes all keys from the cache' do
|
85
|
+
described_class.write('key1', 'value1')
|
86
|
+
described_class.write('key2', 'value2')
|
87
|
+
|
88
|
+
described_class.clear
|
89
|
+
|
90
|
+
expect(described_class.read('key1')).to be_nil
|
91
|
+
expect(described_class.read('key2')).to be_nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
describe WorkOS::Session do
|
4
|
-
let(:user_management) { instance_double('UserManagement') }
|
5
4
|
let(:client_id) { 'test_client_id' }
|
6
5
|
let(:cookie_password) { 'test_very_long_cookie_password__' }
|
7
6
|
let(:session_data) { 'test_session_data' }
|
@@ -10,11 +9,62 @@ describe WorkOS::Session do
|
|
10
9
|
let(:jwk) { JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), { kid: 'sso_oidc_key_pair_123', use: 'sig', alg: 'RS256' }) }
|
11
10
|
|
12
11
|
before do
|
13
|
-
allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url)
|
14
12
|
allow(Net::HTTP).to receive(:get).and_return(jwks_hash)
|
15
13
|
end
|
16
14
|
|
17
15
|
describe 'initialize' do
|
16
|
+
let(:user_management) { instance_double('UserManagement') }
|
17
|
+
|
18
|
+
before do
|
19
|
+
allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url)
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'JWKS caching' do
|
23
|
+
before do
|
24
|
+
WorkOS::Cache.clear
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'caches and returns JWKS' do
|
28
|
+
expect(Net::HTTP).to receive(:get).once
|
29
|
+
session1 = WorkOS::Session.new(
|
30
|
+
user_management: user_management,
|
31
|
+
client_id: client_id,
|
32
|
+
session_data: session_data,
|
33
|
+
cookie_password: cookie_password,
|
34
|
+
)
|
35
|
+
|
36
|
+
session2 = WorkOS::Session.new(
|
37
|
+
user_management: user_management,
|
38
|
+
client_id: client_id,
|
39
|
+
session_data: session_data,
|
40
|
+
cookie_password: cookie_password,
|
41
|
+
)
|
42
|
+
|
43
|
+
expect(session1.jwks.map(&:export)).to eq(session2.jwks.map(&:export))
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'fetches JWKS from remote when cache is expired' do
|
47
|
+
expect(Net::HTTP).to receive(:get).twice
|
48
|
+
session1 = WorkOS::Session.new(
|
49
|
+
user_management: user_management,
|
50
|
+
client_id: client_id,
|
51
|
+
session_data: session_data,
|
52
|
+
cookie_password: cookie_password,
|
53
|
+
)
|
54
|
+
|
55
|
+
allow(Time).to receive(:now).and_return(Time.now + 301)
|
56
|
+
|
57
|
+
session2 = WorkOS::Session.new(
|
58
|
+
user_management: user_management,
|
59
|
+
client_id: client_id,
|
60
|
+
session_data: session_data,
|
61
|
+
cookie_password: cookie_password,
|
62
|
+
)
|
63
|
+
|
64
|
+
expect(session1.jwks.map(&:export)).to eq(session2.jwks.map(&:export))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
18
68
|
it 'raises an error if cookie_password is nil or empty' do
|
19
69
|
expect do
|
20
70
|
WorkOS::Session.new(
|
@@ -52,6 +102,7 @@ describe WorkOS::Session do
|
|
52
102
|
end
|
53
103
|
|
54
104
|
describe '.authenticate' do
|
105
|
+
let(:user_management) { instance_double('UserManagement') }
|
55
106
|
let(:valid_access_token) do
|
56
107
|
payload = {
|
57
108
|
sid: 'session_id',
|
@@ -71,6 +122,10 @@ describe WorkOS::Session do
|
|
71
122
|
}, cookie_password,)
|
72
123
|
end
|
73
124
|
|
125
|
+
before do
|
126
|
+
allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url)
|
127
|
+
end
|
128
|
+
|
74
129
|
it 'returns NO_SESSION_COOKIE_PROVIDED if session_data is nil' do
|
75
130
|
session = WorkOS::Session.new(
|
76
131
|
user_management: user_management,
|
@@ -135,11 +190,13 @@ end
|
|
135
190
|
end
|
136
191
|
|
137
192
|
describe '.refresh' do
|
193
|
+
let(:user_management) { instance_double('UserManagement') }
|
138
194
|
let(:refresh_token) { 'test_refresh_token' }
|
139
195
|
let(:session_data) { WorkOS::Session.seal_data({ refresh_token: refresh_token, user: 'user' }, cookie_password) }
|
140
196
|
let(:auth_response) { double('AuthResponse', sealed_session: 'new_sealed_session') }
|
141
197
|
|
142
198
|
before do
|
199
|
+
allow(user_management).to receive(:get_jwks_url).with(client_id).and_return(jwks_url)
|
143
200
|
allow(user_management).to receive(:authenticate_with_refresh_token).and_return(auth_response)
|
144
201
|
end
|
145
202
|
|
@@ -173,26 +230,33 @@ end
|
|
173
230
|
|
174
231
|
describe '.get_logout_url' do
|
175
232
|
let(:session) do
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
233
|
+
WorkOS::Session.new(
|
234
|
+
user_management: WorkOS::UserManagement,
|
235
|
+
client_id: client_id,
|
236
|
+
session_data: session_data,
|
237
|
+
cookie_password: cookie_password,
|
238
|
+
)
|
239
|
+
end
|
183
240
|
|
184
241
|
context 'when authentication is successful' do
|
185
242
|
before do
|
186
243
|
allow(session).to receive(:authenticate).and_return({
|
187
244
|
authenticated: true,
|
188
|
-
session_id: '
|
245
|
+
session_id: 'session_123abc',
|
189
246
|
reason: nil,
|
190
247
|
})
|
191
|
-
allow(user_management).to receive(:get_logout_url).with(session_id: 'session_id').and_return('https://example.com/logout')
|
192
248
|
end
|
193
249
|
|
194
250
|
it 'returns the logout URL' do
|
195
|
-
expect(session.get_logout_url).to eq('https://
|
251
|
+
expect(session.get_logout_url).to eq('https://api.workos.com/user_management/sessions/logout?session_id=session_123abc')
|
252
|
+
end
|
253
|
+
|
254
|
+
context 'when given a return_to URL' do
|
255
|
+
it 'returns the logout URL with the return_to parameter' do
|
256
|
+
expect(session.get_logout_url(return_to: 'https://example.com/signed-out')).to eq(
|
257
|
+
'https://api.workos.com/user_management/sessions/logout?session_id=session_123abc&return_to=https%3A%2F%2Fexample.com%2Fsigned-out',
|
258
|
+
)
|
259
|
+
end
|
196
260
|
end
|
197
261
|
end
|
198
262
|
|
@@ -1441,4 +1441,25 @@ describe WorkOS::UserManagement do
|
|
1441
1441
|
end
|
1442
1442
|
end
|
1443
1443
|
end
|
1444
|
+
|
1445
|
+
describe '.get_logout_url' do
|
1446
|
+
it 'returns a logout url for the given session ID' do
|
1447
|
+
result = described_class.get_logout_url(
|
1448
|
+
session_id: 'session_01HRX85ATNADY1GQ053AHRFFN6',
|
1449
|
+
)
|
1450
|
+
|
1451
|
+
expect(result).to eq 'https://api.workos.com/user_management/sessions/logout?session_id=session_01HRX85ATNADY1GQ053AHRFFN6'
|
1452
|
+
end
|
1453
|
+
|
1454
|
+
context 'when a `return_to` is given' do
|
1455
|
+
it 'returns a logout url with the `return_to` query parameter' do
|
1456
|
+
result = described_class.get_logout_url(
|
1457
|
+
session_id: 'session_01HRX85ATNADY1GQ053AHRFFN6',
|
1458
|
+
return_to: 'https://example.com/signed-out',
|
1459
|
+
)
|
1460
|
+
|
1461
|
+
expect(result).to eq 'https://api.workos.com/user_management/sessions/logout?session_id=session_01HRX85ATNADY1GQ053AHRFFN6&return_to=https%3A%2F%2Fexample.com%2Fsigned-out'
|
1462
|
+
end
|
1463
|
+
end
|
1464
|
+
end
|
1444
1465
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: workos
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.11.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- WorkOS
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-01-
|
11
|
+
date: 2025-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: encryptor
|
@@ -136,6 +136,7 @@ files:
|
|
136
136
|
- lib/workos/audit_logs.rb
|
137
137
|
- lib/workos/authentication_factor_and_challenge.rb
|
138
138
|
- lib/workos/authentication_response.rb
|
139
|
+
- lib/workos/cache.rb
|
139
140
|
- lib/workos/challenge.rb
|
140
141
|
- lib/workos/client.rb
|
141
142
|
- lib/workos/configuration.rb
|
@@ -184,6 +185,7 @@ files:
|
|
184
185
|
- lib/workos/webhooks.rb
|
185
186
|
- lib/workos/widgets.rb
|
186
187
|
- spec/lib/workos/audit_logs_spec.rb
|
188
|
+
- spec/lib/workos/cache_spec.rb
|
187
189
|
- spec/lib/workos/client.rb
|
188
190
|
- spec/lib/workos/configuration_spec.rb
|
189
191
|
- spec/lib/workos/directory_sync_spec.rb
|
@@ -403,6 +405,7 @@ specification_version: 4
|
|
403
405
|
summary: API client for WorkOS
|
404
406
|
test_files:
|
405
407
|
- spec/lib/workos/audit_logs_spec.rb
|
408
|
+
- spec/lib/workos/cache_spec.rb
|
406
409
|
- spec/lib/workos/client.rb
|
407
410
|
- spec/lib/workos/configuration_spec.rb
|
408
411
|
- spec/lib/workos/directory_sync_spec.rb
|