lex-identity 0.3.0 → 0.4.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -6
  3. data/lex-identity.gemspec +12 -15
  4. data/lib/legion/extensions/identity/actors/credential_refresh.rb +45 -0
  5. data/lib/legion/extensions/identity/actors/orphan_check.rb +48 -0
  6. data/lib/legion/extensions/identity/client.rb +23 -0
  7. data/lib/legion/extensions/identity/helpers/dimensions.rb +71 -0
  8. data/lib/legion/extensions/identity/helpers/fingerprint.rb +166 -0
  9. data/lib/legion/extensions/identity/helpers/graph_client.rb +25 -0
  10. data/lib/legion/extensions/identity/helpers/graph_token.rb +32 -0
  11. data/lib/legion/extensions/identity/helpers/token_cache.rb +55 -0
  12. data/lib/legion/extensions/identity/helpers/vault_secrets.rb +76 -0
  13. data/lib/legion/extensions/identity/local_migrations/20260316000030_create_fingerprint.rb +20 -0
  14. data/lib/legion/extensions/identity/runners/entra.rb +398 -0
  15. data/lib/legion/extensions/identity/runners/identity.rb +86 -0
  16. data/lib/legion/extensions/identity/version.rb +1 -1
  17. data/lib/legion/extensions/identity.rb +14 -8
  18. data/spec/legion/extensions/identity/actors/orphan_check_spec.rb +104 -0
  19. data/spec/legion/extensions/identity/client_spec.rb +32 -0
  20. data/spec/legion/extensions/identity/helpers/dimensions_spec.rb +51 -0
  21. data/spec/legion/extensions/identity/helpers/fingerprint_spec.rb +66 -0
  22. data/spec/legion/extensions/identity/helpers/graph_client_spec.rb +19 -0
  23. data/spec/legion/extensions/identity/helpers/graph_token_spec.rb +31 -0
  24. data/spec/legion/extensions/identity/helpers/token_cache_spec.rb +50 -0
  25. data/spec/legion/extensions/identity/runners/entra_spec.rb +655 -0
  26. data/spec/legion/extensions/identity/runners/identity_spec.rb +61 -0
  27. data/spec/local_persistence_spec.rb +329 -0
  28. data/spec/spec_helper.rb +33 -0
  29. metadata +40 -67
  30. data/CHANGELOG.md +0 -27
  31. data/LICENSE +0 -21
  32. data/README.md +0 -230
  33. data/lib/legion/extensions/identity/helpers/browser.rb +0 -44
  34. data/lib/legion/extensions/identity/helpers/callback_server.rb +0 -170
  35. data/lib/legion/extensions/identity/helpers/oauth.rb +0 -62
  36. data/lib/legion/extensions/identity/helpers/pkce.rb +0 -20
  37. data/lib/legion/extensions/identity/helpers/provider.rb +0 -52
  38. data/lib/legion/extensions/identity/runners/base/oauth.rb +0 -140
  39. data/spec/support/provider_contract.rb +0 -49
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Identity::Helpers::Dimensions do
4
+ describe '.new_identity_model' do
5
+ it 'creates a model with 6 dimensions' do
6
+ model = described_class.new_identity_model
7
+ expect(model.size).to eq(6)
8
+ described_class::IDENTITY_DIMENSIONS.each do |dim|
9
+ expect(model[dim][:mean]).to eq(0.5)
10
+ end
11
+ end
12
+ end
13
+
14
+ describe '.compute_entropy' do
15
+ it 'returns 0.5 for empty observations' do
16
+ model = described_class.new_identity_model
17
+ expect(described_class.compute_entropy({}, model)).to eq(0.5)
18
+ end
19
+
20
+ it 'returns low entropy for observations matching baseline' do
21
+ model = described_class.new_identity_model
22
+ model[:communication_cadence][:observations] = 50
23
+ obs = { communication_cadence: 0.5 }
24
+ entropy = described_class.compute_entropy(obs, model)
25
+ expect(entropy).to be < 0.3
26
+ end
27
+
28
+ it 'returns high entropy for observations diverging from baseline' do
29
+ model = described_class.new_identity_model
30
+ model[:communication_cadence][:observations] = 50
31
+ model[:communication_cadence][:variance] = 0.1
32
+ obs = { communication_cadence: 1.0 }
33
+ entropy = described_class.compute_entropy(obs, model)
34
+ expect(entropy).to be > 0.3
35
+ end
36
+ end
37
+
38
+ describe '.classify_entropy' do
39
+ it 'classifies high entropy' do
40
+ expect(described_class.classify_entropy(0.8)).to eq(:high_entropy)
41
+ end
42
+
43
+ it 'classifies low entropy' do
44
+ expect(described_class.classify_entropy(0.1)).to eq(:low_entropy)
45
+ end
46
+
47
+ it 'classifies normal entropy' do
48
+ expect(described_class.classify_entropy(0.5)).to eq(:normal)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Identity::Helpers::Fingerprint do
4
+ let(:fp) { described_class.new }
5
+
6
+ describe '#observe' do
7
+ it 'records observation for valid dimension' do
8
+ fp.observe(:communication_cadence, 0.7)
9
+ expect(fp.observation_count).to eq(1)
10
+ end
11
+
12
+ it 'ignores invalid dimensions' do
13
+ fp.observe(:nonexistent, 0.5)
14
+ expect(fp.observation_count).to eq(0)
15
+ end
16
+
17
+ it 'shifts mean toward observed values' do
18
+ original = fp.model[:vocabulary_patterns][:mean]
19
+ 10.times { fp.observe(:vocabulary_patterns, 0.9) }
20
+ expect(fp.model[:vocabulary_patterns][:mean]).to be > original
21
+ end
22
+ end
23
+
24
+ describe '#observe_all' do
25
+ it 'records multiple dimensions at once' do
26
+ fp.observe_all(communication_cadence: 0.6, vocabulary_patterns: 0.7)
27
+ expect(fp.observation_count).to eq(2)
28
+ end
29
+ end
30
+
31
+ describe '#current_entropy' do
32
+ it 'computes entropy against model' do
33
+ entropy = fp.current_entropy(communication_cadence: 0.5)
34
+ expect(entropy).to be_between(0.0, 1.0)
35
+ end
36
+
37
+ it 'tracks entropy history' do
38
+ 3.times { fp.current_entropy(communication_cadence: 0.5) }
39
+ expect(fp.entropy_history.size).to eq(3)
40
+ end
41
+ end
42
+
43
+ describe '#entropy_trend' do
44
+ it 'returns stable for insufficient data' do
45
+ expect(fp.entropy_trend).to eq(:stable)
46
+ end
47
+
48
+ it 'detects rising entropy' do
49
+ 5.times { |i| fp.current_entropy(communication_cadence: 0.5 + (i * 0.1)) }
50
+ # Trend depends on actual computed values
51
+ trend = fp.entropy_trend(window: 5)
52
+ expect(%i[rising stable falling]).to include(trend)
53
+ end
54
+ end
55
+
56
+ describe '#maturity' do
57
+ it 'starts as nascent' do
58
+ expect(fp.maturity).to eq(:nascent)
59
+ end
60
+
61
+ it 'progresses to developing' do
62
+ 15.times { fp.observe(:communication_cadence, 0.5) }
63
+ expect(fp.maturity).to eq(:developing)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/identity/helpers/graph_client'
5
+
6
+ RSpec.describe Legion::Extensions::Identity::Helpers::GraphClient do
7
+ describe '.connection' do
8
+ it 'returns a Faraday connection with bearer token' do
9
+ conn = described_class.connection(token: 'test-token')
10
+ expect(conn).to be_a(Faraday::Connection)
11
+ expect(conn.headers['Authorization']).to eq('Bearer test-token')
12
+ end
13
+
14
+ it 'uses custom base URL when provided' do
15
+ conn = described_class.connection(token: 'tok', base: 'https://custom.api.com')
16
+ expect(conn.url_prefix.to_s).to eq('https://custom.api.com/')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/identity/helpers/graph_token'
5
+
6
+ RSpec.describe Legion::Extensions::Identity::Helpers::GraphToken do
7
+ describe '.fetch' do
8
+ it 'raises GraphTokenError on failure' do
9
+ stub_request = instance_double(Faraday::Response, success?: false,
10
+ body: { 'error_description' => 'invalid_client' })
11
+ conn = instance_double(Faraday::Connection)
12
+ allow(conn).to receive(:post).and_return(stub_request)
13
+ allow(Faraday).to receive(:new).and_return(conn)
14
+
15
+ expect do
16
+ described_class.fetch(tenant_id: 't1', client_id: 'c1', client_secret: 's1')
17
+ end.to raise_error(described_class::GraphTokenError, 'invalid_client')
18
+ end
19
+
20
+ it 'returns access_token on success' do
21
+ stub_request = instance_double(Faraday::Response, success?: true,
22
+ body: { 'access_token' => 'tok-123' })
23
+ conn = instance_double(Faraday::Connection)
24
+ allow(conn).to receive(:post).and_return(stub_request)
25
+ allow(Faraday).to receive(:new).and_return(conn)
26
+
27
+ token = described_class.fetch(tenant_id: 't1', client_id: 'c1', client_secret: 's1')
28
+ expect(token).to eq('tok-123')
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/identity/helpers/token_cache'
5
+
6
+ RSpec.describe Legion::Extensions::Identity::Helpers::TokenCache do
7
+ before { described_class.clear_all }
8
+ after { described_class.clear_all }
9
+
10
+ describe '.store and .fetch' do
11
+ it 'stores and retrieves a token' do
12
+ described_class.store(worker_id: 'w1', token: 'abc', expires_in: 3600)
13
+ entry = described_class.fetch(worker_id: 'w1')
14
+ expect(entry[:access_token]).to eq('abc')
15
+ end
16
+
17
+ it 'returns nil for unknown worker' do
18
+ expect(described_class.fetch(worker_id: 'unknown')).to be_nil
19
+ end
20
+
21
+ it 'returns nil for expired token' do
22
+ described_class.store(worker_id: 'w1', token: 'abc', expires_in: -1)
23
+ expect(described_class.fetch(worker_id: 'w1')).to be_nil
24
+ end
25
+ end
26
+
27
+ describe '.approaching_expiry?' do
28
+ it 'returns true when no token exists' do
29
+ expect(described_class.approaching_expiry?(worker_id: 'w1')).to be true
30
+ end
31
+
32
+ it 'returns true when token is within buffer' do
33
+ described_class.store(worker_id: 'w1', token: 'abc', expires_in: 100)
34
+ expect(described_class.approaching_expiry?(worker_id: 'w1', buffer: 200)).to be true
35
+ end
36
+
37
+ it 'returns false when token has plenty of time' do
38
+ described_class.store(worker_id: 'w1', token: 'abc', expires_in: 3600)
39
+ expect(described_class.approaching_expiry?(worker_id: 'w1', buffer: 300)).to be false
40
+ end
41
+ end
42
+
43
+ describe '.clear' do
44
+ it 'removes a specific worker token' do
45
+ described_class.store(worker_id: 'w1', token: 'abc', expires_in: 3600)
46
+ described_class.clear(worker_id: 'w1')
47
+ expect(described_class.fetch(worker_id: 'w1')).to be_nil
48
+ end
49
+ end
50
+ end