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.
- checksums.yaml +4 -4
- data/Gemfile +4 -6
- data/lex-identity.gemspec +12 -15
- data/lib/legion/extensions/identity/actors/credential_refresh.rb +45 -0
- data/lib/legion/extensions/identity/actors/orphan_check.rb +48 -0
- data/lib/legion/extensions/identity/client.rb +23 -0
- data/lib/legion/extensions/identity/helpers/dimensions.rb +71 -0
- data/lib/legion/extensions/identity/helpers/fingerprint.rb +166 -0
- data/lib/legion/extensions/identity/helpers/graph_client.rb +25 -0
- data/lib/legion/extensions/identity/helpers/graph_token.rb +32 -0
- data/lib/legion/extensions/identity/helpers/token_cache.rb +55 -0
- data/lib/legion/extensions/identity/helpers/vault_secrets.rb +76 -0
- data/lib/legion/extensions/identity/local_migrations/20260316000030_create_fingerprint.rb +20 -0
- data/lib/legion/extensions/identity/runners/entra.rb +398 -0
- data/lib/legion/extensions/identity/runners/identity.rb +86 -0
- data/lib/legion/extensions/identity/version.rb +1 -1
- data/lib/legion/extensions/identity.rb +14 -8
- data/spec/legion/extensions/identity/actors/orphan_check_spec.rb +104 -0
- data/spec/legion/extensions/identity/client_spec.rb +32 -0
- data/spec/legion/extensions/identity/helpers/dimensions_spec.rb +51 -0
- data/spec/legion/extensions/identity/helpers/fingerprint_spec.rb +66 -0
- data/spec/legion/extensions/identity/helpers/graph_client_spec.rb +19 -0
- data/spec/legion/extensions/identity/helpers/graph_token_spec.rb +31 -0
- data/spec/legion/extensions/identity/helpers/token_cache_spec.rb +50 -0
- data/spec/legion/extensions/identity/runners/entra_spec.rb +655 -0
- data/spec/legion/extensions/identity/runners/identity_spec.rb +61 -0
- data/spec/local_persistence_spec.rb +329 -0
- data/spec/spec_helper.rb +33 -0
- metadata +40 -67
- data/CHANGELOG.md +0 -27
- data/LICENSE +0 -21
- data/README.md +0 -230
- data/lib/legion/extensions/identity/helpers/browser.rb +0 -44
- data/lib/legion/extensions/identity/helpers/callback_server.rb +0 -170
- data/lib/legion/extensions/identity/helpers/oauth.rb +0 -62
- data/lib/legion/extensions/identity/helpers/pkce.rb +0 -20
- data/lib/legion/extensions/identity/helpers/provider.rb +0 -52
- data/lib/legion/extensions/identity/runners/base/oauth.rb +0 -140
- data/spec/support/provider_contract.rb +0 -49
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/identity/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Identity::Runners::Identity do
|
|
6
|
+
let(:client) { Legion::Extensions::Identity::Client.new }
|
|
7
|
+
|
|
8
|
+
describe '#observe_behavior' do
|
|
9
|
+
it 'records a single observation' do
|
|
10
|
+
result = client.observe_behavior(dimension: :communication_cadence, value: 0.7)
|
|
11
|
+
expect(result[:recorded]).to be true
|
|
12
|
+
expect(result[:observation_count]).to eq(1)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '#observe_all' do
|
|
17
|
+
it 'records multiple observations' do
|
|
18
|
+
result = client.observe_all(observations: {
|
|
19
|
+
communication_cadence: 0.6,
|
|
20
|
+
vocabulary_patterns: 0.7,
|
|
21
|
+
emotional_response: 0.5
|
|
22
|
+
})
|
|
23
|
+
expect(result[:dimensions_observed].size).to eq(3)
|
|
24
|
+
expect(result[:observation_count]).to eq(3)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#check_entropy' do
|
|
29
|
+
it 'returns entropy classification' do
|
|
30
|
+
result = client.check_entropy
|
|
31
|
+
expect(result[:entropy]).to be_between(0.0, 1.0)
|
|
32
|
+
expect(result).to have_key(:classification)
|
|
33
|
+
expect(result).to have_key(:trend)
|
|
34
|
+
expect(result).to have_key(:in_range)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'warns on high entropy observations' do
|
|
38
|
+
# Build baseline
|
|
39
|
+
20.times { client.observe_behavior(dimension: :communication_cadence, value: 0.5) }
|
|
40
|
+
# Check with very different observation
|
|
41
|
+
result = client.check_entropy(observations: { communication_cadence: 10.0 })
|
|
42
|
+
expect(result[:warning]).to eq(:possible_impersonation_or_drift) if result[:classification] == :high_entropy
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe '#identity_status' do
|
|
47
|
+
it 'returns model state' do
|
|
48
|
+
status = client.identity_status
|
|
49
|
+
expect(status).to have_key(:model)
|
|
50
|
+
expect(status).to have_key(:maturity)
|
|
51
|
+
expect(status).to have_key(:observation_count)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '#identity_maturity' do
|
|
56
|
+
it 'returns maturity level' do
|
|
57
|
+
result = client.identity_maturity
|
|
58
|
+
expect(result[:maturity]).to eq(:nascent)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Local persistence spec for Legion::Extensions::Identity::Helpers::Fingerprint
|
|
4
|
+
#
|
|
5
|
+
# Strategy: stub Legion::Data::Local with a real in-memory Sequel SQLite database
|
|
6
|
+
# when sequel + sqlite3 are available, otherwise use a double that records calls.
|
|
7
|
+
# Either way, the Fingerprint save/load logic is exercised end-to-end.
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
require 'sequel'
|
|
11
|
+
require 'sqlite3'
|
|
12
|
+
SEQUEL_AVAILABLE = true
|
|
13
|
+
rescue LoadError
|
|
14
|
+
SEQUEL_AVAILABLE = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
RSpec.describe Legion::Extensions::Identity::Helpers::Fingerprint, 'local persistence' do
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Helpers for setting up the in-memory DB (shared between contexts)
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def build_in_memory_db
|
|
23
|
+
db = Sequel.sqlite
|
|
24
|
+
db.create_table(:identity_fingerprint) do
|
|
25
|
+
primary_key :id
|
|
26
|
+
String :dimension, null: false, unique: true
|
|
27
|
+
Float :mean, default: 0.0
|
|
28
|
+
Float :variance, default: 0.0
|
|
29
|
+
Integer :observations, default: 0
|
|
30
|
+
DateTime :last_observed
|
|
31
|
+
end
|
|
32
|
+
db.create_table(:identity_meta) do
|
|
33
|
+
primary_key :id
|
|
34
|
+
Integer :observation_count, default: 0
|
|
35
|
+
String :entropy_history, text: true
|
|
36
|
+
end
|
|
37
|
+
db
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def stub_local(db)
|
|
41
|
+
local_mod = Module.new do
|
|
42
|
+
define_singleton_method(:connection) { db }
|
|
43
|
+
define_singleton_method(:connected?) { true }
|
|
44
|
+
end
|
|
45
|
+
stub_const('Legion::Data', Module.new)
|
|
46
|
+
stub_const('Legion::Data::Local', local_mod)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# When Sequel + sqlite3 are available: full round-trip integration tests
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
if SEQUEL_AVAILABLE
|
|
54
|
+
context 'with an in-memory SQLite database' do
|
|
55
|
+
let(:db) { build_in_memory_db }
|
|
56
|
+
|
|
57
|
+
before { stub_local(db) }
|
|
58
|
+
|
|
59
|
+
describe '#save_to_local' do
|
|
60
|
+
it 'returns true on first save' do
|
|
61
|
+
fp = described_class.new
|
|
62
|
+
expect(fp.save_to_local).to be true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'persists all 6 dimension rows to identity_fingerprint' do
|
|
66
|
+
fp = described_class.new
|
|
67
|
+
fp.observe(:communication_cadence, 0.8)
|
|
68
|
+
fp.save_to_local
|
|
69
|
+
|
|
70
|
+
rows = db[:identity_fingerprint].all
|
|
71
|
+
expect(rows.size).to eq(6)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'persists updated mean for observed dimension' do
|
|
75
|
+
fp = described_class.new
|
|
76
|
+
10.times { fp.observe(:vocabulary_patterns, 0.9) }
|
|
77
|
+
fp.save_to_local
|
|
78
|
+
|
|
79
|
+
row = db[:identity_fingerprint].where(dimension: 'vocabulary_patterns').first
|
|
80
|
+
expect(row[:mean]).to be > 0.5
|
|
81
|
+
expect(row[:observations]).to eq(10)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'persists observation_count in identity_meta' do
|
|
85
|
+
fp = described_class.new
|
|
86
|
+
3.times { fp.observe(:communication_cadence, 0.6) }
|
|
87
|
+
fp.save_to_local
|
|
88
|
+
|
|
89
|
+
meta = db[:identity_meta].first
|
|
90
|
+
expect(meta[:observation_count]).to eq(3)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'persists entropy_history as JSON in identity_meta' do
|
|
94
|
+
fp = described_class.new
|
|
95
|
+
fp.current_entropy(communication_cadence: 0.5)
|
|
96
|
+
fp.current_entropy(vocabulary_patterns: 0.6)
|
|
97
|
+
fp.save_to_local
|
|
98
|
+
|
|
99
|
+
meta = db[:identity_meta].first
|
|
100
|
+
parsed = JSON.parse(meta[:entropy_history])
|
|
101
|
+
expect(parsed.size).to eq(2)
|
|
102
|
+
expect(parsed.first).to have_key('entropy')
|
|
103
|
+
expect(parsed.first).to have_key('at')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'updates existing rows on second save (upsert)' do
|
|
107
|
+
fp = described_class.new
|
|
108
|
+
fp.observe(:emotional_response, 0.3)
|
|
109
|
+
fp.save_to_local
|
|
110
|
+
|
|
111
|
+
# mutate and save again
|
|
112
|
+
5.times { fp.observe(:emotional_response, 0.9) }
|
|
113
|
+
fp.save_to_local
|
|
114
|
+
|
|
115
|
+
rows = db[:identity_fingerprint].where(dimension: 'emotional_response').all
|
|
116
|
+
expect(rows.size).to eq(1) # still one row, not two
|
|
117
|
+
expect(rows.first[:observations]).to eq(6)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe '#load_from_local' do
|
|
122
|
+
it 'returns true when called with an empty DB' do
|
|
123
|
+
fp = described_class.new # load_from_local called in initialize
|
|
124
|
+
expect(fp.observation_count).to eq(0)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'restores model dimensions from DB rows' do
|
|
128
|
+
# Pre-seed DB directly
|
|
129
|
+
db[:identity_fingerprint].insert(
|
|
130
|
+
dimension: 'communication_cadence',
|
|
131
|
+
mean: 0.75, variance: 0.05, observations: 42,
|
|
132
|
+
last_observed: Time.now.utc
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
fp = described_class.new # triggers load_from_local
|
|
136
|
+
expect(fp.model[:communication_cadence][:mean]).to be_within(0.001).of(0.75)
|
|
137
|
+
expect(fp.model[:communication_cadence][:observations]).to eq(42)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'restores observation_count from identity_meta' do
|
|
141
|
+
db[:identity_meta].insert(observation_count: 57, entropy_history: '[]')
|
|
142
|
+
|
|
143
|
+
fp = described_class.new
|
|
144
|
+
expect(fp.observation_count).to eq(57)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'restores entropy_history from identity_meta JSON' do
|
|
148
|
+
history = [
|
|
149
|
+
{ 'entropy' => 0.3, 'at' => Time.now.utc.iso8601 },
|
|
150
|
+
{ 'entropy' => 0.4, 'at' => Time.now.utc.iso8601 }
|
|
151
|
+
]
|
|
152
|
+
db[:identity_meta].insert(observation_count: 0, entropy_history: JSON.generate(history))
|
|
153
|
+
|
|
154
|
+
fp = described_class.new
|
|
155
|
+
expect(fp.entropy_history.size).to eq(2)
|
|
156
|
+
expect(fp.entropy_history.first[:entropy]).to be_within(0.001).of(0.3)
|
|
157
|
+
expect(fp.entropy_history.first[:at]).to be_a(Time)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'ignores DB rows for unknown dimensions' do
|
|
161
|
+
db[:identity_fingerprint].insert(
|
|
162
|
+
dimension: 'nonexistent_dimension',
|
|
163
|
+
mean: 0.9, variance: 0.1, observations: 5,
|
|
164
|
+
last_observed: nil
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
fp = described_class.new # must not raise
|
|
168
|
+
expect(fp.model.keys).to match_array(
|
|
169
|
+
%i[communication_cadence vocabulary_patterns emotional_response
|
|
170
|
+
decision_patterns contextual_consistency temporal_patterns]
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
describe 'full round-trip' do
|
|
176
|
+
it 'survives a save-then-load cycle with identical state' do
|
|
177
|
+
# Build state in first fingerprint instance
|
|
178
|
+
fp1 = described_class.new
|
|
179
|
+
12.times { fp1.observe(:communication_cadence, 0.7) }
|
|
180
|
+
8.times { fp1.observe(:vocabulary_patterns, 0.6) }
|
|
181
|
+
fp1.current_entropy(communication_cadence: 0.65)
|
|
182
|
+
fp1.current_entropy(vocabulary_patterns: 0.55)
|
|
183
|
+
fp1.save_to_local
|
|
184
|
+
|
|
185
|
+
# Load into a fresh instance (DB already populated)
|
|
186
|
+
fp2 = described_class.new
|
|
187
|
+
|
|
188
|
+
expect(fp2.observation_count).to eq(fp1.observation_count)
|
|
189
|
+
expect(fp2.entropy_history.size).to eq(fp1.entropy_history.size)
|
|
190
|
+
|
|
191
|
+
dims = %i[communication_cadence vocabulary_patterns]
|
|
192
|
+
dims.each do |dim|
|
|
193
|
+
expect(fp2.model[dim][:mean]).to be_within(0.0001).of(fp1.model[dim][:mean])
|
|
194
|
+
expect(fp2.model[dim][:variance]).to be_within(0.0001).of(fp1.model[dim][:variance])
|
|
195
|
+
expect(fp2.model[dim][:observations]).to eq(fp1.model[dim][:observations])
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'preserves maturity after round-trip' do
|
|
200
|
+
fp1 = described_class.new
|
|
201
|
+
15.times { fp1.observe(:decision_patterns, 0.5) }
|
|
202
|
+
expect(fp1.maturity).to eq(:developing)
|
|
203
|
+
fp1.save_to_local
|
|
204
|
+
|
|
205
|
+
fp2 = described_class.new
|
|
206
|
+
expect(fp2.maturity).to eq(:developing)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it 'preserves entropy trend direction after round-trip' do
|
|
210
|
+
fp1 = described_class.new
|
|
211
|
+
# Ascending entropy values — second half larger than first half
|
|
212
|
+
[0.1, 0.1, 0.1, 0.1, 0.7, 0.8, 0.9, 0.95, 0.98, 1.0].each do |e|
|
|
213
|
+
fp1.instance_variable_get(:@entropy_history) << { entropy: e, at: Time.now.utc }
|
|
214
|
+
end
|
|
215
|
+
fp1.save_to_local
|
|
216
|
+
|
|
217
|
+
fp2 = described_class.new
|
|
218
|
+
expect(fp2.entropy_history.map { |h| h[:entropy] }).to eq(
|
|
219
|
+
fp1.entropy_history.map { |h| h[:entropy] }
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
else
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
# Fallback: double-based tests when Sequel is not in the bundle
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
context 'when Sequel is not available (double-based fallback)' do
|
|
231
|
+
let(:fingerprint_rows) { {} }
|
|
232
|
+
let(:meta_rows) { [] }
|
|
233
|
+
|
|
234
|
+
let(:fp_dataset) do
|
|
235
|
+
d = double('fingerprint_dataset')
|
|
236
|
+
allow(d).to receive(:where) { |args|
|
|
237
|
+
scoped = double('scoped_fp_dataset')
|
|
238
|
+
allow(scoped).to receive(:first) { fingerprint_rows[args[:dimension]] }
|
|
239
|
+
allow(scoped).to receive(:update) { |row|
|
|
240
|
+
fingerprint_rows[row[:dimension] || args[:dimension]] = row
|
|
241
|
+
}
|
|
242
|
+
scoped
|
|
243
|
+
}
|
|
244
|
+
allow(d).to receive(:insert) { |row| fingerprint_rows[row[:dimension]] = row }
|
|
245
|
+
allow(d).to receive(:each) { |&block| fingerprint_rows.each_value(&block) }
|
|
246
|
+
allow(d).to receive(:first) { fingerprint_rows.values.first }
|
|
247
|
+
d
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
let(:meta_dataset) do
|
|
251
|
+
d = double('meta_dataset')
|
|
252
|
+
allow(d).to receive(:first) { meta_rows.first }
|
|
253
|
+
allow(d).to receive(:insert) { |row| meta_rows << row }
|
|
254
|
+
allow(d).to receive(:where) do
|
|
255
|
+
scoped = double('scoped_meta_dataset')
|
|
256
|
+
allow(scoped).to receive(:update) { |row| meta_rows[0] = meta_rows[0]&.merge(row) }
|
|
257
|
+
scoped
|
|
258
|
+
end
|
|
259
|
+
d
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
let(:db) do
|
|
263
|
+
d = double('Sequel::Database')
|
|
264
|
+
allow(d).to receive(:[]).with(:identity_fingerprint).and_return(fp_dataset)
|
|
265
|
+
allow(d).to receive(:[]).with(:identity_meta).and_return(meta_dataset)
|
|
266
|
+
d
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
before do
|
|
270
|
+
local_mod = Module.new do
|
|
271
|
+
define_singleton_method(:connection) { nil } # overridden per example
|
|
272
|
+
define_singleton_method(:connected?) { true }
|
|
273
|
+
end
|
|
274
|
+
stub_const('Legion::Data', Module.new)
|
|
275
|
+
stub_const('Legion::Data::Local', local_mod)
|
|
276
|
+
allow(Legion::Data::Local).to receive(:connection).and_return(db)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it 'calls insert on the fingerprint dataset during save_to_local' do
|
|
280
|
+
fp = described_class.new
|
|
281
|
+
expect(fp_dataset).to receive(:insert).at_least(:once)
|
|
282
|
+
fp.save_to_local
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it 'calls insert on the meta dataset during save_to_local' do
|
|
286
|
+
fp = described_class.new
|
|
287
|
+
expect(meta_dataset).to receive(:insert).once
|
|
288
|
+
fp.save_to_local
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
it 'does not raise when local is unavailable (Legion::Data::Local not defined)' do
|
|
292
|
+
# Remove the stub so defined? check returns false
|
|
293
|
+
hide_const('Legion::Data::Local')
|
|
294
|
+
expect { described_class.new }.not_to raise_error
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# Behaviour when Legion::Data::Local is not defined (always run)
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
describe 'when Legion::Data::Local is not available' do
|
|
304
|
+
before do
|
|
305
|
+
hide_const('Legion::Data::Local') if defined?(Legion::Data::Local)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it 'initialize completes without error' do
|
|
309
|
+
expect { described_class.new }.not_to raise_error
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it 'save_to_local returns nil (guard short-circuits)' do
|
|
313
|
+
fp = described_class.new
|
|
314
|
+
expect(fp.save_to_local).to be_nil
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
it 'load_from_local returns nil (guard short-circuits)' do
|
|
318
|
+
fp = described_class.new
|
|
319
|
+
expect(fp.load_from_local).to be_nil
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it 'model starts with fresh defaults' do
|
|
323
|
+
fp = described_class.new
|
|
324
|
+
expect(fp.model[:communication_cadence][:mean]).to eq(0.5)
|
|
325
|
+
expect(fp.observation_count).to eq(0)
|
|
326
|
+
expect(fp.entropy_history).to be_empty
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
# Stub framework classes not available outside the full Legion runtime
|
|
6
|
+
module Legion
|
|
7
|
+
module Logging
|
|
8
|
+
def self.debug(_msg); end
|
|
9
|
+
|
|
10
|
+
def self.info(_msg); end
|
|
11
|
+
|
|
12
|
+
def self.warn(_msg); end
|
|
13
|
+
|
|
14
|
+
def self.error(_msg); end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module Extensions
|
|
18
|
+
module Actors
|
|
19
|
+
class Every; end # rubocop:disable Lint/EmptyClass
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Prevent re-require of actor base when identity.rb loads orphan_check
|
|
25
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
26
|
+
|
|
27
|
+
require 'legion/extensions/identity'
|
|
28
|
+
|
|
29
|
+
RSpec.configure do |config|
|
|
30
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
31
|
+
config.disable_monkey_patching!
|
|
32
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
33
|
+
end
|
metadata
CHANGED
|
@@ -1,112 +1,85 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-identity
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Esity
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - "~>"
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '2.0'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - "~>"
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '2.0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: legion-cache
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: 1.3.0
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: 1.3.0
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: legion-json
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - ">="
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: 1.2.1
|
|
47
|
-
type: :runtime
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - ">="
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: 1.2.1
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: legion-logging
|
|
13
|
+
name: sequel
|
|
56
14
|
requirement: !ruby/object:Gem::Requirement
|
|
57
15
|
requirements:
|
|
58
16
|
- - ">="
|
|
59
17
|
- !ruby/object:Gem::Version
|
|
60
|
-
version:
|
|
61
|
-
type: :
|
|
18
|
+
version: '5.70'
|
|
19
|
+
type: :development
|
|
62
20
|
prerelease: false
|
|
63
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
22
|
requirements:
|
|
65
23
|
- - ">="
|
|
66
24
|
- !ruby/object:Gem::Version
|
|
67
|
-
version:
|
|
25
|
+
version: '5.70'
|
|
68
26
|
- !ruby/object:Gem::Dependency
|
|
69
|
-
name:
|
|
27
|
+
name: sqlite3
|
|
70
28
|
requirement: !ruby/object:Gem::Requirement
|
|
71
29
|
requirements:
|
|
72
30
|
- - ">="
|
|
73
31
|
- !ruby/object:Gem::Version
|
|
74
|
-
version:
|
|
75
|
-
type: :
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :development
|
|
76
34
|
prerelease: false
|
|
77
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
36
|
requirements:
|
|
79
37
|
- - ">="
|
|
80
38
|
- !ruby/object:Gem::Version
|
|
81
|
-
version:
|
|
82
|
-
description:
|
|
83
|
-
|
|
39
|
+
version: '2.0'
|
|
40
|
+
description: Human partner identity modeling and behavioral entropy for brain-modeled
|
|
41
|
+
agentic AI
|
|
42
|
+
email:
|
|
43
|
+
- matthewdiverson@gmail.com
|
|
84
44
|
executables: []
|
|
85
45
|
extensions: []
|
|
86
46
|
extra_rdoc_files: []
|
|
87
47
|
files:
|
|
88
|
-
- CHANGELOG.md
|
|
89
48
|
- Gemfile
|
|
90
|
-
- LICENSE
|
|
91
|
-
- README.md
|
|
92
49
|
- lex-identity.gemspec
|
|
93
50
|
- lib/legion/extensions/identity.rb
|
|
94
|
-
- lib/legion/extensions/identity/
|
|
95
|
-
- lib/legion/extensions/identity/
|
|
96
|
-
- lib/legion/extensions/identity/
|
|
97
|
-
- lib/legion/extensions/identity/helpers/
|
|
98
|
-
- lib/legion/extensions/identity/helpers/
|
|
99
|
-
- lib/legion/extensions/identity/
|
|
51
|
+
- lib/legion/extensions/identity/actors/credential_refresh.rb
|
|
52
|
+
- lib/legion/extensions/identity/actors/orphan_check.rb
|
|
53
|
+
- lib/legion/extensions/identity/client.rb
|
|
54
|
+
- lib/legion/extensions/identity/helpers/dimensions.rb
|
|
55
|
+
- lib/legion/extensions/identity/helpers/fingerprint.rb
|
|
56
|
+
- lib/legion/extensions/identity/helpers/graph_client.rb
|
|
57
|
+
- lib/legion/extensions/identity/helpers/graph_token.rb
|
|
58
|
+
- lib/legion/extensions/identity/helpers/token_cache.rb
|
|
59
|
+
- lib/legion/extensions/identity/helpers/vault_secrets.rb
|
|
60
|
+
- lib/legion/extensions/identity/local_migrations/20260316000030_create_fingerprint.rb
|
|
61
|
+
- lib/legion/extensions/identity/runners/entra.rb
|
|
62
|
+
- lib/legion/extensions/identity/runners/identity.rb
|
|
100
63
|
- lib/legion/extensions/identity/version.rb
|
|
101
|
-
- spec/
|
|
64
|
+
- spec/legion/extensions/identity/actors/orphan_check_spec.rb
|
|
65
|
+
- spec/legion/extensions/identity/client_spec.rb
|
|
66
|
+
- spec/legion/extensions/identity/helpers/dimensions_spec.rb
|
|
67
|
+
- spec/legion/extensions/identity/helpers/fingerprint_spec.rb
|
|
68
|
+
- spec/legion/extensions/identity/helpers/graph_client_spec.rb
|
|
69
|
+
- spec/legion/extensions/identity/helpers/graph_token_spec.rb
|
|
70
|
+
- spec/legion/extensions/identity/helpers/token_cache_spec.rb
|
|
71
|
+
- spec/legion/extensions/identity/runners/entra_spec.rb
|
|
72
|
+
- spec/legion/extensions/identity/runners/identity_spec.rb
|
|
73
|
+
- spec/local_persistence_spec.rb
|
|
74
|
+
- spec/spec_helper.rb
|
|
102
75
|
homepage: https://github.com/LegionIO/lex-identity
|
|
103
76
|
licenses:
|
|
104
77
|
- MIT
|
|
105
78
|
metadata:
|
|
106
79
|
homepage_uri: https://github.com/LegionIO/lex-identity
|
|
107
80
|
source_code_uri: https://github.com/LegionIO/lex-identity
|
|
108
|
-
documentation_uri: https://github.com/LegionIO/lex-identity
|
|
109
|
-
changelog_uri: https://github.com/LegionIO/lex-identity
|
|
81
|
+
documentation_uri: https://github.com/LegionIO/lex-identity
|
|
82
|
+
changelog_uri: https://github.com/LegionIO/lex-identity
|
|
110
83
|
bug_tracker_uri: https://github.com/LegionIO/lex-identity/issues
|
|
111
84
|
rubygems_mfa_required: 'true'
|
|
112
85
|
rdoc_options: []
|
|
@@ -125,5 +98,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
125
98
|
requirements: []
|
|
126
99
|
rubygems_version: 3.6.9
|
|
127
100
|
specification_version: 4
|
|
128
|
-
summary:
|
|
101
|
+
summary: LEX Identity
|
|
129
102
|
test_files: []
|
data/CHANGELOG.md
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## [0.3.0] - 2026-11-09
|
|
4
|
-
|
|
5
|
-
### Added
|
|
6
|
-
- Explicit provider-neutral browser login combining PKCE, authorization, a state-correlated loopback callback, and token exchange.
|
|
7
|
-
- A single-use callback listener with bounded reads/writes, request limits, optional fixed-port binding, and cleanup on every exit.
|
|
8
|
-
- Callback security and lifecycle specs for invalid state, ambiguous parameters, provider denial, stalled clients, timeout, and listener disposal.
|
|
9
|
-
|
|
10
|
-
### Changed
|
|
11
|
-
- Moved reusable OAuth runners to `runners/base/oauth.rb` so generic endpoint and browser operations are not automatically exposed through extension API routes. Consumers must explicitly require the new path.
|
|
12
|
-
|
|
13
|
-
## [0.2.0] - 2026-11-07
|
|
14
|
-
|
|
15
|
-
### Added
|
|
16
|
-
- Provider integration helpers for resolver registration, lease construction, and broker credential access using one provider name.
|
|
17
|
-
- Optional PKCE generation and browser helpers.
|
|
18
|
-
- Provider-neutral OAuth authorization, code exchange, refresh, and deadline-bounded device-code polling.
|
|
19
|
-
- Reusable provider conformance examples covering the core resolver's metadata and resolution contracts.
|
|
20
|
-
- Shared OAuth runner compatible with standard extension discovery; the base extension is not itself an identity provider or remotely invocable.
|
|
21
|
-
|
|
22
|
-
## [0.1.0] - 2026-11-07
|
|
23
|
-
|
|
24
|
-
### Added
|
|
25
|
-
- Initial gem packaging, namespace, and version.
|
|
26
|
-
- Standard actors, runners, helpers, and spec directories.
|
|
27
|
-
- Shared CI/release workflow, dependency updates, and RuboCop/RSpec configuration.
|
data/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 LegionIO
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|