standard_id 0.16.1 → 0.17.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/lib/standard_id/config/callable_validator.rb +2 -2
- data/lib/standard_id/config/schema.rb +3 -3
- data/lib/standard_id/config_schema.rb +211 -0
- data/lib/standard_id/engine.rb +32 -1
- data/lib/standard_id/events/subscribers/passwordless_delivery_subscriber.rb +6 -0
- data/lib/standard_id/passwordless/base_strategy.rb +9 -3
- data/lib/standard_id/provider_registry.rb +4 -6
- data/lib/standard_id/providers/base.rb +1 -1
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id.rb +7 -3
- metadata +5 -9
- data/lib/standard_config/config.rb +0 -73
- data/lib/standard_config/config_provider.rb +0 -82
- data/lib/standard_config/manager.rb +0 -93
- data/lib/standard_config/schema.rb +0 -139
- data/lib/standard_config.rb +0 -45
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50cac29c9420971a28c8c4b401349cc0563270a16d519c0abd9052da600c685a
|
|
4
|
+
data.tar.gz: b47c1a99cc5d7d335f9dee1c53d4831cbcc70b3d933bb6130e600797eb4998df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04e2bf5d6503e312fecdedfdee8f5eb7b1dd5c438c25f351ab222160249bb4de40d3b7348bffc4431d71047e05cb918ea399e67060ad66d946d17dc66d038ce8
|
|
7
|
+
data.tar.gz: 6f38767fc4bd02d3595cf243bf1d326881cf17baea355577416158bf43418a6423f7d3a14c9c2fa721b38c9f1ff4fa75658cd5b0b2f26e48d6fb970cc041c3cb
|
|
@@ -18,8 +18,8 @@ module StandardId
|
|
|
18
18
|
# runtime.
|
|
19
19
|
#
|
|
20
20
|
# Scope: only validates top-level StandardId.config fields plus the hash
|
|
21
|
-
# entries under `oauth.claim_resolvers`. Does NOT modify the
|
|
22
|
-
#
|
|
21
|
+
# entries under `oauth.claim_resolvers`. Does NOT modify the schema
|
|
22
|
+
# (the gem's schema doesn't need to know about this).
|
|
23
23
|
module CallableValidator
|
|
24
24
|
# Each entry: field_path => { signature: "(a, b, c)", arity: Integer, kind: :positional | :keyword, keywords: [..] }
|
|
25
25
|
# Field path is the method chain on StandardId.config, e.g. "after_sign_in"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Schema definitions for StandardId
|
|
2
2
|
# This file defines the configuration schema structure
|
|
3
3
|
|
|
4
|
-
require "
|
|
4
|
+
require "standard_id/config_schema"
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
StandardId::ConfigSchema.define do
|
|
7
7
|
scope :base do
|
|
8
8
|
field :account_class_name, type: :string, default: "User"
|
|
9
9
|
field :cache_store, type: :any, default: nil
|
|
@@ -147,7 +147,7 @@ StandardConfig.schema.draw do
|
|
|
147
147
|
#
|
|
148
148
|
# Note: the scope is named `reset_password` rather than `password_reset` to
|
|
149
149
|
# avoid a name collision with the `web.password_reset` boolean feature flag
|
|
150
|
-
# —
|
|
150
|
+
# — StandardId::ConfigSchema resolves unique field names globally.
|
|
151
151
|
field :delivery, type: :symbol, default: :custom
|
|
152
152
|
field :mailer_from, type: :string, default: "noreply@example.com"
|
|
153
153
|
field :mailer_subject, type: :string, default: "Reset your password"
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
require "active_support/ordered_options"
|
|
2
|
+
require "concurrent/map"
|
|
3
|
+
|
|
4
|
+
module StandardId
|
|
5
|
+
# Lightweight configuration schema backed by ActiveSupport::OrderedOptions.
|
|
6
|
+
# Replaces the vendored `StandardConfig` DSL/manager. Fields are declared
|
|
7
|
+
# per scope; the resulting top-level config exposes each scope as a nested
|
|
8
|
+
# OrderedOptions and routes any field whose name is unique across scopes
|
|
9
|
+
# to the owning scope (so host apps can read base-scope fields like
|
|
10
|
+
# `config.account_class_name` without the `base.` prefix).
|
|
11
|
+
class ConfigSchema
|
|
12
|
+
Field = Struct.new(:name, :type, :default) do
|
|
13
|
+
def default_value
|
|
14
|
+
return default.call if default.respond_to?(:call)
|
|
15
|
+
return default.dup if default.is_a?(Array) || default.is_a?(Hash)
|
|
16
|
+
default
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def instance = (@instance ||= new)
|
|
22
|
+
def define(&block) = instance.define(&block)
|
|
23
|
+
def add_field(**kwargs) = instance.add_field(**kwargs)
|
|
24
|
+
def build = instance.apply(Config.new)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize = @scopes = Concurrent::Map.new
|
|
28
|
+
def scopes = @scopes
|
|
29
|
+
def scope?(name) = @scopes.key?(name.to_sym)
|
|
30
|
+
def field?(scope_name, field_name) = !!@scopes[scope_name.to_sym]&.key?(field_name.to_sym)
|
|
31
|
+
def field_for(scope_name, field_name) = @scopes[scope_name.to_sym]&.[](field_name.to_sym)
|
|
32
|
+
|
|
33
|
+
def define(&block)
|
|
34
|
+
DSL.new(self).instance_eval(&block) if block
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def add_field(scope:, name:, type: :string, default: nil)
|
|
39
|
+
fields = ensure_scope(scope)
|
|
40
|
+
fields.compute_if_absent(name.to_sym) { Field.new(name.to_sym, type, default) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Register a scope without adding a field. Allows `define { scope :foo }` so
|
|
44
|
+
# provider gems can later `add_field(scope: :foo, ...)` against an existing scope.
|
|
45
|
+
def ensure_scope(name)
|
|
46
|
+
@scopes.compute_if_absent(name.to_sym) { Concurrent::Map.new }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Scopes that declare a field with the given name (used for top-level routing).
|
|
50
|
+
def scopes_with_field(field_name)
|
|
51
|
+
sym = field_name.to_sym
|
|
52
|
+
@scopes.each_pair.with_object([]) { |(s, fs), acc| acc << s if fs.key?(sym) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Populate the given Config with scope sub-options + defaults. Re-apply is safe;
|
|
56
|
+
# values already set in a Scope are preserved (so provider gems can register
|
|
57
|
+
# fields after host apps have set base values).
|
|
58
|
+
def apply(config)
|
|
59
|
+
config.__schema__ = self
|
|
60
|
+
@scopes.each_pair do |scope_name, fields|
|
|
61
|
+
opts = (config.key?(scope_name) && config[scope_name].is_a?(Scope)) ? config[scope_name] : Scope.new(self, scope_name)
|
|
62
|
+
fields.each_value { |f| opts.write_default(f.name, f.default_value) }
|
|
63
|
+
config.write_raw(scope_name, opts)
|
|
64
|
+
end
|
|
65
|
+
config
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def cast(value, type)
|
|
69
|
+
return value if value.nil?
|
|
70
|
+
case type
|
|
71
|
+
when :any then value
|
|
72
|
+
when :symbol then value.is_a?(Symbol) ? value : value.to_sym
|
|
73
|
+
when :string then value.to_s
|
|
74
|
+
when :integer then value.to_i
|
|
75
|
+
when :float then value.to_f
|
|
76
|
+
when :array then Array(value)
|
|
77
|
+
when :hash then value.is_a?(Hash) ? value : {}
|
|
78
|
+
when :boolean
|
|
79
|
+
case value
|
|
80
|
+
when true, false then value
|
|
81
|
+
when "true", "1", 1 then true
|
|
82
|
+
when "false", "0", 0 then false
|
|
83
|
+
else !!value
|
|
84
|
+
end
|
|
85
|
+
else value
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# DSL: `define { scope :base do field :foo, type: :string, default: "x" end }`.
|
|
90
|
+
# Anonymous-class form keeps both levels in one place; #scope yields a sub-DSL
|
|
91
|
+
# that closes over the parent schema + scope name.
|
|
92
|
+
class DSL
|
|
93
|
+
def initialize(schema, scope_name = nil)
|
|
94
|
+
@schema = schema
|
|
95
|
+
@scope_name = scope_name
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def scope(name, &block)
|
|
99
|
+
@schema.ensure_scope(name)
|
|
100
|
+
DSL.new(@schema, name.to_sym).instance_eval(&block) if block
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def field(name, type: :string, default: nil, **)
|
|
104
|
+
@schema.add_field(scope: @scope_name, name: name, type: type, default: default)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Per-scope OrderedOptions. Validates writes, casts and dups Array/Hash values
|
|
109
|
+
# on read. When `resolver` is set (via `Config#register`), reads delegate to
|
|
110
|
+
# the resolver-returned hash for dynamic / multi-tenant configuration.
|
|
111
|
+
class Scope < ActiveSupport::OrderedOptions
|
|
112
|
+
RAW_SET = ActiveSupport::OrderedOptions.instance_method(:[]=)
|
|
113
|
+
private_constant :RAW_SET
|
|
114
|
+
|
|
115
|
+
attr_accessor :resolver
|
|
116
|
+
|
|
117
|
+
def initialize(schema, scope_name)
|
|
118
|
+
super()
|
|
119
|
+
@schema = schema
|
|
120
|
+
@scope_name = scope_name
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def []=(key, value)
|
|
124
|
+
validate!(key)
|
|
125
|
+
super(key.to_sym, value)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def [](key)
|
|
129
|
+
sym = key.to_sym
|
|
130
|
+
validate!(sym) unless key?(sym) || resolver
|
|
131
|
+
raw = if resolver
|
|
132
|
+
hash = resolver.call || {}
|
|
133
|
+
if hash.respond_to?(:key?) && hash.respond_to?(:[])
|
|
134
|
+
hash.key?(sym) ? hash[sym] : hash[sym.to_s]
|
|
135
|
+
end
|
|
136
|
+
elsif key?(sym)
|
|
137
|
+
super(sym)
|
|
138
|
+
else
|
|
139
|
+
@schema.field_for(@scope_name, sym)&.default_value
|
|
140
|
+
end
|
|
141
|
+
cast_read(sym, raw)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def write_default(key, value)
|
|
145
|
+
return if key?(key.to_sym)
|
|
146
|
+
RAW_SET.bind_call(self, key.to_sym, value)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def validate!(key)
|
|
152
|
+
return if @schema.field?(@scope_name, key)
|
|
153
|
+
raise StandardId::ConfigurationError,
|
|
154
|
+
"Unknown field '#{key}' for scope '#{@scope_name}'. Valid fields: #{@schema.scopes[@scope_name]&.keys}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def cast_read(key, value)
|
|
158
|
+
field = @schema.field_for(@scope_name, key)
|
|
159
|
+
return value unless field
|
|
160
|
+
casted = @schema.cast(value, field.type)
|
|
161
|
+
casted.is_a?(Array) || casted.is_a?(Hash) ? casted.dup : casted
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Top-level config: routes unqualified field reads/writes to the owning scope
|
|
166
|
+
# when the name is unique across scopes.
|
|
167
|
+
class Config < ActiveSupport::OrderedOptions
|
|
168
|
+
RAW_SET = ActiveSupport::OrderedOptions.instance_method(:[]=)
|
|
169
|
+
private_constant :RAW_SET
|
|
170
|
+
|
|
171
|
+
attr_accessor :__schema__
|
|
172
|
+
|
|
173
|
+
def register(scope_name, resolver)
|
|
174
|
+
sym = scope_name.to_sym
|
|
175
|
+
unless __schema__&.scope?(sym)
|
|
176
|
+
raise ArgumentError, "Unknown configuration scope: #{sym}. Valid scopes: #{__schema__&.scopes&.keys}"
|
|
177
|
+
end
|
|
178
|
+
self[sym].resolver = resolver
|
|
179
|
+
self
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def registered?(scope_name) = !!self[scope_name.to_sym]&.resolver
|
|
183
|
+
|
|
184
|
+
def [](key)
|
|
185
|
+
sym = key.to_sym
|
|
186
|
+
return super if key?(sym) || __schema__.nil? || __schema__.scope?(sym)
|
|
187
|
+
target = unique_scope_for(sym)
|
|
188
|
+
target ? self[target][sym] : super
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def []=(key, value)
|
|
192
|
+
sym = key.to_sym
|
|
193
|
+
if __schema__ && !__schema__.scope?(sym) && !key?(sym) && (target = unique_scope_for(sym))
|
|
194
|
+
self[target][sym] = value
|
|
195
|
+
else
|
|
196
|
+
super(sym, value)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Bypass routing — used by the schema applier.
|
|
201
|
+
def write_raw(key, value) = RAW_SET.bind_call(self, key.to_sym, value)
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
def unique_scope_for(name)
|
|
206
|
+
matches = __schema__.scopes_with_field(name)
|
|
207
|
+
matches.size == 1 ? matches.first : nil
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
data/lib/standard_id/engine.rb
CHANGED
|
@@ -59,8 +59,18 @@ module StandardId
|
|
|
59
59
|
# We warn (not raise) to avoid breaking apps that intentionally short-
|
|
60
60
|
# circuit boot (e.g., `assets:precompile` rake tasks with no secrets
|
|
61
61
|
# available). A hard failure would be hostile to those workflows.
|
|
62
|
+
#
|
|
63
|
+
# IMPORTANT: we must NOT call `app.secret_key_base` directly. Rails 8.1's
|
|
64
|
+
# getter runs the "generate + persist" path on first read, which in turn
|
|
65
|
+
# invokes the setter — and the setter raises when the resolved value is
|
|
66
|
+
# blank (which is exactly the case this check is meant to warn about).
|
|
67
|
+
# So calling the getter here would turn a soft warning into a hard boot
|
|
68
|
+
# failure under parallel workers (e.g. parallel_rspec's `parallel:create`)
|
|
69
|
+
# that don't share a generated key between processes. Instead we read
|
|
70
|
+
# the same underlying sources Rails resolves from (ENV, then credentials)
|
|
71
|
+
# without triggering the write path.
|
|
62
72
|
def self.verify_host_cookie_encryption!(app)
|
|
63
|
-
secret = app
|
|
73
|
+
secret = host_secret_key_base(app)
|
|
64
74
|
|
|
65
75
|
if secret.blank?
|
|
66
76
|
Rails.logger.warn(
|
|
@@ -73,6 +83,27 @@ module StandardId
|
|
|
73
83
|
end
|
|
74
84
|
end
|
|
75
85
|
|
|
86
|
+
# Probe secret_key_base sources without touching the memoizing setter.
|
|
87
|
+
# Mirrors Rails' own resolution order (ENV -> credentials), stopping on
|
|
88
|
+
# the first present value. Returns nil when neither is set — the caller
|
|
89
|
+
# logs a warning in that case.
|
|
90
|
+
def self.host_secret_key_base(app)
|
|
91
|
+
return ENV["SECRET_KEY_BASE"] if ENV["SECRET_KEY_BASE"].present?
|
|
92
|
+
|
|
93
|
+
credentials = app.respond_to?(:credentials) ? app.credentials : nil
|
|
94
|
+
return nil unless credentials&.respond_to?(:secret_key_base)
|
|
95
|
+
|
|
96
|
+
credentials.secret_key_base
|
|
97
|
+
rescue StandardError
|
|
98
|
+
# A broken credentials file or missing master key raises during read.
|
|
99
|
+
# That's the host app's problem to surface elsewhere — here we just
|
|
100
|
+
# want the cookie-encryption warning to fire, so we treat "could not
|
|
101
|
+
# probe" as "not configured" and let Rails error loudly on its own
|
|
102
|
+
# path if the app genuinely needs the secret.
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
private_class_method :host_secret_key_base
|
|
106
|
+
|
|
76
107
|
# Logs a production-only warning when no global audience allow-list is
|
|
77
108
|
# configured. With `allowed_audiences` empty, the API token manager
|
|
78
109
|
# skips decode-time aud enforcement, leaving cross-audience JWT replay
|
|
@@ -5,6 +5,12 @@ module StandardId
|
|
|
5
5
|
subscribe_to StandardId::Events::PASSWORDLESS_CODE_GENERATED
|
|
6
6
|
|
|
7
7
|
def call(event)
|
|
8
|
+
# Per-call manual-delivery request takes precedence over the global
|
|
9
|
+
# delivery config. Honors Otp.issue(delivery: :manual) callers who
|
|
10
|
+
# deliver the code themselves (custom widget flows, step-up
|
|
11
|
+
# challenges, etc.) and would otherwise receive a duplicate email
|
|
12
|
+
# from this subscriber when c.passwordless.delivery == :built_in.
|
|
13
|
+
return if event[:skip_sender]
|
|
8
14
|
return unless built_in_delivery?
|
|
9
15
|
return unless event[:channel] == "email"
|
|
10
16
|
|
|
@@ -30,7 +30,12 @@ module StandardId
|
|
|
30
30
|
expires_in: expires_in,
|
|
31
31
|
metadata: metadata
|
|
32
32
|
)
|
|
33
|
-
|
|
33
|
+
# skip_sender is forwarded into the event payload so subscribers that
|
|
34
|
+
# deliver on PASSWORDLESS_CODE_GENERATED (e.g. PasswordlessDeliverySubscriber)
|
|
35
|
+
# can honor a per-call manual-delivery request — not just the legacy
|
|
36
|
+
# sender_callback. Without this, Otp.issue(delivery: :manual) silently
|
|
37
|
+
# double-delivers when c.passwordless.delivery == :built_in.
|
|
38
|
+
emit_code_generated(challenge, username, skip_sender: skip_sender)
|
|
34
39
|
sender_callback&.call(username, challenge.code) unless skip_sender
|
|
35
40
|
emit_code_sent(username) unless skip_sender
|
|
36
41
|
challenge
|
|
@@ -158,14 +163,15 @@ module StandardId
|
|
|
158
163
|
)
|
|
159
164
|
end
|
|
160
165
|
|
|
161
|
-
def emit_code_generated(challenge, username)
|
|
166
|
+
def emit_code_generated(challenge, username, skip_sender: false)
|
|
162
167
|
StandardId::Events.publish(
|
|
163
168
|
StandardId::Events::PASSWORDLESS_CODE_GENERATED,
|
|
164
169
|
code_challenge: challenge,
|
|
165
170
|
identifier: username,
|
|
166
171
|
channel: connection_type,
|
|
167
172
|
realm: @realm,
|
|
168
|
-
expires_at: challenge.expires_at
|
|
173
|
+
expires_at: challenge.expires_at,
|
|
174
|
+
skip_sender: skip_sender
|
|
169
175
|
)
|
|
170
176
|
end
|
|
171
177
|
|
|
@@ -34,7 +34,6 @@ module StandardId
|
|
|
34
34
|
)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
|
|
38
37
|
# Get all registered providers
|
|
39
38
|
# @return [Hash] Provider name => class mapping
|
|
40
39
|
def all
|
|
@@ -50,16 +49,15 @@ module StandardId
|
|
|
50
49
|
|
|
51
50
|
private
|
|
52
51
|
|
|
53
|
-
# Register provider's config schema fields with
|
|
52
|
+
# Register provider's config schema fields with the StandardId schema.
|
|
53
|
+
# Thread-safe and idempotent — adding the same field twice is a no-op.
|
|
54
54
|
# @param provider_class [Class] Provider implementation class
|
|
55
55
|
def register_config_schema(provider_class)
|
|
56
56
|
schema = provider_class.config_schema
|
|
57
57
|
return if schema.nil? || schema.empty?
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
field field_name, **options
|
|
62
|
-
end
|
|
59
|
+
schema.each do |field_name, options|
|
|
60
|
+
StandardId::ConfigSchema.add_field(scope: :social, name: field_name, **options)
|
|
63
61
|
end
|
|
64
62
|
end
|
|
65
63
|
|
|
@@ -106,7 +106,7 @@ module StandardId
|
|
|
106
106
|
|
|
107
107
|
# Define configuration schema fields for this provider.
|
|
108
108
|
#
|
|
109
|
-
# Returns a hash of field definitions compatible with
|
|
109
|
+
# Returns a hash of field definitions compatible with the StandardId::ConfigSchema DSL.
|
|
110
110
|
# These fields will be registered under the :social configuration scope.
|
|
111
111
|
#
|
|
112
112
|
# @return [Hash] Field definitions with types and defaults
|
data/lib/standard_id/version.rb
CHANGED
data/lib/standard_id.rb
CHANGED
|
@@ -3,6 +3,7 @@ require "standard_id/current_attributes"
|
|
|
3
3
|
require "standard_id/engine"
|
|
4
4
|
require "standard_id/web_engine"
|
|
5
5
|
require "standard_id/api_engine"
|
|
6
|
+
require "standard_id/config_schema"
|
|
6
7
|
require "standard_id/config/schema"
|
|
7
8
|
require "standard_id/scope_config"
|
|
8
9
|
require "standard_id/errors"
|
|
@@ -59,20 +60,23 @@ require "standard_id/providers/base"
|
|
|
59
60
|
require "standard_id/provider_registry"
|
|
60
61
|
|
|
61
62
|
module StandardId
|
|
63
|
+
CONFIG = Concurrent::Delay.new { ConfigSchema.build }
|
|
64
|
+
|
|
62
65
|
class << self
|
|
63
66
|
CACHE_STORE = Concurrent::Delay.new { config.cache_store || Rails.cache }
|
|
64
67
|
LOGGER = Concurrent::Delay.new { config.logger || Rails.logger }
|
|
65
68
|
|
|
66
69
|
def configure(&block)
|
|
67
|
-
|
|
70
|
+
yield config if block_given?
|
|
71
|
+
config
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
def register(scope_name, resolver_proc)
|
|
71
|
-
|
|
75
|
+
config.register(scope_name, resolver_proc)
|
|
72
76
|
end
|
|
73
77
|
|
|
74
78
|
def config
|
|
75
|
-
|
|
79
|
+
CONFIG.value
|
|
76
80
|
end
|
|
77
81
|
|
|
78
82
|
def cache_store
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: standard_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.17.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -13,14 +13,14 @@ dependencies:
|
|
|
13
13
|
name: rails
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '8.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '8.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
@@ -215,11 +215,6 @@ files:
|
|
|
215
215
|
- db/migrate/20260416180511_add_partial_indexes_for_active_session_and_challenge_lookups.rb
|
|
216
216
|
- lib/generators/standard_id/install/install_generator.rb
|
|
217
217
|
- lib/generators/standard_id/install/templates/standard_id.rb
|
|
218
|
-
- lib/standard_config.rb
|
|
219
|
-
- lib/standard_config/config.rb
|
|
220
|
-
- lib/standard_config/config_provider.rb
|
|
221
|
-
- lib/standard_config/manager.rb
|
|
222
|
-
- lib/standard_config/schema.rb
|
|
223
218
|
- lib/standard_id.rb
|
|
224
219
|
- lib/standard_id/account_locking.rb
|
|
225
220
|
- lib/standard_id/account_status.rb
|
|
@@ -232,6 +227,7 @@ files:
|
|
|
232
227
|
- lib/standard_id/config/callable_validator.rb
|
|
233
228
|
- lib/standard_id/config/schema.rb
|
|
234
229
|
- lib/standard_id/config/scope_claims_validator.rb
|
|
230
|
+
- lib/standard_id/config_schema.rb
|
|
235
231
|
- lib/standard_id/current_attributes.rb
|
|
236
232
|
- lib/standard_id/engine.rb
|
|
237
233
|
- lib/standard_id/errors.rb
|
|
@@ -305,7 +301,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
305
301
|
requirements:
|
|
306
302
|
- - ">="
|
|
307
303
|
- !ruby/object:Gem::Version
|
|
308
|
-
version: '
|
|
304
|
+
version: '4.0'
|
|
309
305
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
310
306
|
requirements:
|
|
311
307
|
- - ">="
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
module StandardConfig
|
|
2
|
-
# Manages configuration for the StandardId engine
|
|
3
|
-
#
|
|
4
|
-
# Usage:
|
|
5
|
-
# StandardId.configure do |config|
|
|
6
|
-
# config.account_class_name = "User"
|
|
7
|
-
# config.cache_store = ActiveSupport::Cache::MemoryStore.new
|
|
8
|
-
# config.logger = Rails.logger
|
|
9
|
-
# config.allowed_post_logout_redirect_uris = ["https://example.com/logout"]
|
|
10
|
-
# end
|
|
11
|
-
class Config
|
|
12
|
-
# The name of the Account model class as a String, e.g. "User" or "Account"
|
|
13
|
-
attr_accessor :account_class_name
|
|
14
|
-
|
|
15
|
-
# Optional cache store and logger, used by StandardId.cache_store and StandardId.logger
|
|
16
|
-
attr_accessor :cache_store, :logger
|
|
17
|
-
|
|
18
|
-
# OAuth issuer identifier for ID tokens
|
|
19
|
-
attr_accessor :issuer
|
|
20
|
-
|
|
21
|
-
# Optional login URL for redirecting unauthenticated browser requests
|
|
22
|
-
# Example: "/login" or a full URL like "https://app.example.com/login"
|
|
23
|
-
# If set, Authorization endpoints can redirect to this path with a redirect_uri param
|
|
24
|
-
attr_accessor :login_url
|
|
25
|
-
|
|
26
|
-
# Social login hooks
|
|
27
|
-
attr_accessor :social_account_attributes
|
|
28
|
-
|
|
29
|
-
# Passwordless authentication delivery callbacks (deprecated - use events instead)
|
|
30
|
-
attr_accessor :passwordless_email_sender, :passwordless_sms_sender
|
|
31
|
-
|
|
32
|
-
# Allowed post-logout redirect URIs for OIDC logout endpoint
|
|
33
|
-
# If empty or nil, no redirects are allowed and the endpoint will return a JSON message
|
|
34
|
-
# If provided, the post_logout_redirect_uri must exactly match one of the values in this list
|
|
35
|
-
attr_accessor :allowed_post_logout_redirect_uris
|
|
36
|
-
|
|
37
|
-
# Layout name to use for StandardId Web controllers.
|
|
38
|
-
# If nil, controllers should default to "application" (host app or dummy app).
|
|
39
|
-
# Examples: "application", "standard_id/web/application", "my_custom_layout"
|
|
40
|
-
attr_accessor :web_layout
|
|
41
|
-
|
|
42
|
-
# Enable Inertia.js rendering for StandardId Web controllers
|
|
43
|
-
# When true and inertia_rails gem is available, controllers will render Inertia components
|
|
44
|
-
attr_accessor :use_inertia
|
|
45
|
-
|
|
46
|
-
# Namespace prefix for Inertia component paths
|
|
47
|
-
# Example: "Auth" will generate component paths like "Auth/Login/show"
|
|
48
|
-
attr_accessor :inertia_component_namespace
|
|
49
|
-
|
|
50
|
-
def initialize
|
|
51
|
-
@account_class_name = nil
|
|
52
|
-
@cache_store = nil
|
|
53
|
-
@logger = nil
|
|
54
|
-
@issuer = nil
|
|
55
|
-
@login_url = nil
|
|
56
|
-
@apple_key_id = nil
|
|
57
|
-
@apple_team_id = nil
|
|
58
|
-
@social_account_attributes = nil
|
|
59
|
-
@passwordless_email_sender = nil
|
|
60
|
-
@passwordless_sms_sender = nil
|
|
61
|
-
@allowed_post_logout_redirect_uris = []
|
|
62
|
-
@web_layout = nil
|
|
63
|
-
@use_inertia = nil
|
|
64
|
-
@inertia_component_namespace = nil
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def account_class
|
|
68
|
-
account_class_name.constantize
|
|
69
|
-
rescue NameError
|
|
70
|
-
raise NameError, "Could not find account class: #{account_class_name}. Please set a valid class name using `StandardId.configure { |c| c.account_class_name = 'YourAccountClass' }`"
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
require "ostruct"
|
|
2
|
-
|
|
3
|
-
module StandardConfig
|
|
4
|
-
class ConfigProvider
|
|
5
|
-
def initialize(scope_name, resolver_proc, schema = nil)
|
|
6
|
-
@scope_name = scope_name
|
|
7
|
-
@resolver_proc = resolver_proc
|
|
8
|
-
@schema = schema
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def method_missing(method_name, *args)
|
|
12
|
-
if method_name.to_s.end_with?("=")
|
|
13
|
-
# Setter - only works for static configs (OpenStruct objects)
|
|
14
|
-
field_name = method_name.to_s.chomp("=").to_sym
|
|
15
|
-
validate_field!(field_name)
|
|
16
|
-
|
|
17
|
-
config_object = @resolver_proc.call
|
|
18
|
-
if config_object.respond_to?(method_name)
|
|
19
|
-
config_object.send(method_name, args.first)
|
|
20
|
-
elsif config_object.respond_to?(:[]=)
|
|
21
|
-
# Support hash-like providers
|
|
22
|
-
value = args.first
|
|
23
|
-
config_object[field_name] = value
|
|
24
|
-
# Also set string key for convenience if symbol not used by provider
|
|
25
|
-
begin
|
|
26
|
-
config_object[field_name.to_s] = value
|
|
27
|
-
rescue StandardError
|
|
28
|
-
# ignore if provider doesn't accept string keys
|
|
29
|
-
end
|
|
30
|
-
else
|
|
31
|
-
raise NoMethodError, "Configuration object doesn't support setting #{field_name}"
|
|
32
|
-
end
|
|
33
|
-
else
|
|
34
|
-
# Getter
|
|
35
|
-
get_field(method_name)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def get_field(field_name)
|
|
40
|
-
validate_field!(field_name)
|
|
41
|
-
|
|
42
|
-
config_object = @resolver_proc.call
|
|
43
|
-
raw_value = if config_object.respond_to?(field_name)
|
|
44
|
-
config_object.send(field_name)
|
|
45
|
-
elsif config_object.respond_to?(:[])
|
|
46
|
-
config_object[field_name] || config_object[field_name.to_s]
|
|
47
|
-
else
|
|
48
|
-
nil
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Cast the value according to schema
|
|
52
|
-
field_def = @schema&.field_definition(@scope_name, field_name)
|
|
53
|
-
return raw_value unless field_def
|
|
54
|
-
|
|
55
|
-
casted = @schema&.cast_value(raw_value, field_def.type) || raw_value
|
|
56
|
-
# Return dup for mutable structures to prevent accidental mutation of shared defaults
|
|
57
|
-
if casted.is_a?(Array)
|
|
58
|
-
casted.dup
|
|
59
|
-
elsif casted.is_a?(Hash)
|
|
60
|
-
casted.dup
|
|
61
|
-
else
|
|
62
|
-
casted
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def respond_to_missing?(method_name, include_private = false)
|
|
67
|
-
field_name = method_name.to_s.end_with?("=") ? method_name.to_s.chomp("=").to_sym : method_name.to_sym
|
|
68
|
-
@schema&.valid_field?(@scope_name, field_name) || super
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
def validate_field!(field_name)
|
|
74
|
-
return unless @schema # Skip validation if no schema provided
|
|
75
|
-
|
|
76
|
-
unless @schema.valid_field?(@scope_name, field_name)
|
|
77
|
-
valid_fields = @schema.scopes[@scope_name]&.fields&.keys || []
|
|
78
|
-
raise ArgumentError, "Unknown field '#{field_name}' for scope '#{@scope_name}'. Valid fields: #{valid_fields}"
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
require "ostruct"
|
|
2
|
-
require "concurrent/map"
|
|
3
|
-
require "standard_config/config_provider"
|
|
4
|
-
|
|
5
|
-
module StandardConfig
|
|
6
|
-
class Manager
|
|
7
|
-
def initialize(schema)
|
|
8
|
-
@schema = schema
|
|
9
|
-
@providers = Concurrent::Map.new
|
|
10
|
-
@static_configs = Concurrent::Map.new
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Register a configuration provider for a scope
|
|
14
|
-
def register(scope_name, resolver_proc)
|
|
15
|
-
scope_name = scope_name.to_sym
|
|
16
|
-
|
|
17
|
-
# Validate scope exists in schema
|
|
18
|
-
unless @schema.valid_scope?(scope_name)
|
|
19
|
-
raise ArgumentError, "Unknown configuration scope: #{scope_name}. Valid scopes: #{@schema.scopes.keys}"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
@providers[scope_name] = ConfigProvider.new(scope_name, resolver_proc, @schema)
|
|
23
|
-
self
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def registered?(scope_name)
|
|
27
|
-
@providers.key?(scope_name.to_sym)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Access configuration scopes via method calls
|
|
31
|
-
def method_missing(method_name, *args)
|
|
32
|
-
method_str = method_name.to_s
|
|
33
|
-
scope_name = method_str.end_with?("=") ? method_str.chomp("=").to_sym : method_name.to_sym
|
|
34
|
-
|
|
35
|
-
# Handle field setter via unique scope resolution
|
|
36
|
-
if method_str.end_with?("=")
|
|
37
|
-
field = scope_name
|
|
38
|
-
scopes = @schema.scopes_with_field(field)
|
|
39
|
-
if scopes.size == 1
|
|
40
|
-
s = scopes.first
|
|
41
|
-
provider = @providers.compute_if_absent(s) do
|
|
42
|
-
ConfigProvider.new(s, -> { create_static_config_for_scope(s) }, @schema)
|
|
43
|
-
end
|
|
44
|
-
provider.public_send(method_name, *args)
|
|
45
|
-
return args.first
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Handle field getter via unique scope resolution
|
|
50
|
-
scopes = @schema.scopes_with_field(scope_name)
|
|
51
|
-
if scopes.size == 1
|
|
52
|
-
s = scopes.first
|
|
53
|
-
provider = @providers.compute_if_absent(s) do
|
|
54
|
-
ConfigProvider.new(s, -> { create_static_config_for_scope(s) }, @schema)
|
|
55
|
-
end
|
|
56
|
-
return provider.get_field(scope_name)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Handle scope access
|
|
60
|
-
provider = @providers[scope_name]
|
|
61
|
-
return provider if provider
|
|
62
|
-
|
|
63
|
-
# Create static provider for valid scopes on first access
|
|
64
|
-
if @schema.valid_scope?(scope_name)
|
|
65
|
-
return @providers.compute_if_absent(scope_name) do
|
|
66
|
-
ConfigProvider.new(scope_name, -> { create_static_config_for_scope(scope_name) }, @schema)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
super
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def respond_to_missing?(method_name, include_private = false)
|
|
74
|
-
method_str = method_name.to_s
|
|
75
|
-
scope_name = method_str.end_with?("=") ? method_str.chomp("=").to_sym : method_name.to_sym
|
|
76
|
-
@schema.valid_scope?(scope_name) ||
|
|
77
|
-
@schema.scopes_with_field(scope_name).any? ||
|
|
78
|
-
super
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
private
|
|
82
|
-
|
|
83
|
-
def create_static_config_for_scope(scope_name)
|
|
84
|
-
@static_configs.compute_if_absent(scope_name) do
|
|
85
|
-
OpenStruct.new.tap do |config|
|
|
86
|
-
@schema.scopes[scope_name].fields.each do |field_name, field_def|
|
|
87
|
-
config.send("#{field_name}=", field_def.default_value)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
require "concurrent/map"
|
|
2
|
-
|
|
3
|
-
module StandardConfig
|
|
4
|
-
class Schema
|
|
5
|
-
def initialize
|
|
6
|
-
@scopes = Concurrent::Map.new
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
# DSL entry
|
|
10
|
-
def draw(&block)
|
|
11
|
-
Drawer.new(self).instance_eval(&block) if block_given?
|
|
12
|
-
self
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def scopes
|
|
16
|
-
@scopes
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def scope(name, &block)
|
|
20
|
-
name_sym = name.to_sym
|
|
21
|
-
builder = scopes.compute_if_absent(name_sym) { ScopeBuilder.new(name_sym) }
|
|
22
|
-
builder.instance_eval(&block) if block_given?
|
|
23
|
-
builder
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def valid_scope?(name)
|
|
27
|
-
scopes.key?(name.to_sym)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def valid_field?(scope_name, field_name)
|
|
31
|
-
return false unless valid_scope?(scope_name)
|
|
32
|
-
scopes[scope_name.to_sym].fields.key?(field_name.to_sym)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def field_definition(scope_name, field_name)
|
|
36
|
-
return nil unless valid_scope?(scope_name)
|
|
37
|
-
scopes[scope_name.to_sym].fields[field_name.to_sym]
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Return an array of scope names that define the given field
|
|
41
|
-
def scopes_with_field(field_name)
|
|
42
|
-
scopes.keys.select { |s| scopes[s].fields.key?(field_name.to_sym) }
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def cast_value(value, type)
|
|
46
|
-
return value if value.nil?
|
|
47
|
-
|
|
48
|
-
case type
|
|
49
|
-
when :any
|
|
50
|
-
value
|
|
51
|
-
when :string
|
|
52
|
-
value.to_s
|
|
53
|
-
when :integer
|
|
54
|
-
value.to_i
|
|
55
|
-
when :float
|
|
56
|
-
value.to_f
|
|
57
|
-
when :boolean
|
|
58
|
-
case value
|
|
59
|
-
when true, false then value
|
|
60
|
-
when "true", "1", 1 then true
|
|
61
|
-
when "false", "0", 0 then false
|
|
62
|
-
else !!value
|
|
63
|
-
end
|
|
64
|
-
when :array
|
|
65
|
-
Array(value)
|
|
66
|
-
when :hash
|
|
67
|
-
value.is_a?(Hash) ? value : {}
|
|
68
|
-
else
|
|
69
|
-
value
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
class ScopeBuilder
|
|
74
|
-
attr_reader :name, :fields
|
|
75
|
-
|
|
76
|
-
def initialize(name)
|
|
77
|
-
@name = name.to_sym
|
|
78
|
-
@fields = Concurrent::Map.new
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def field(name, type: :string, default: nil, readonly: false)
|
|
82
|
-
key = name.to_sym
|
|
83
|
-
if @fields.key?(key)
|
|
84
|
-
Kernel.warn("[StandardId::Configuration] Redefining field '#{key}' in scope '#{@name}'. Last definition wins.")
|
|
85
|
-
end
|
|
86
|
-
@fields[key] = FieldDefinition.new(name, type: type, default: default, readonly: readonly)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
class FieldDefinition
|
|
91
|
-
attr_reader :name, :type, :default, :readonly
|
|
92
|
-
|
|
93
|
-
def initialize(name, type: :string, default: nil, readonly: false)
|
|
94
|
-
@name = name.to_sym
|
|
95
|
-
@type = type
|
|
96
|
-
@default = default
|
|
97
|
-
@readonly = readonly
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def default_value
|
|
101
|
-
if @default.respond_to?(:call)
|
|
102
|
-
@default.call
|
|
103
|
-
elsif @default.is_a?(Array)
|
|
104
|
-
@default.dup
|
|
105
|
-
else
|
|
106
|
-
@default
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Internal DSL driver
|
|
112
|
-
class Drawer
|
|
113
|
-
def initialize(schema)
|
|
114
|
-
@schema = schema
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# scope :base do ... end OR scope :passwordless do ... end
|
|
118
|
-
def scope(name, &block)
|
|
119
|
-
name_sym = name.to_sym
|
|
120
|
-
# Ensure scope exists, then evaluate the block in a scoped context
|
|
121
|
-
@schema.scope(name_sym)
|
|
122
|
-
ScopedScope.new(@schema, name_sym).instance_eval(&block) if block_given?
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
class ScopedScope
|
|
127
|
-
def initialize(schema, scope_name)
|
|
128
|
-
@schema = schema
|
|
129
|
-
@scope_name = scope_name
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def field(name, type: :string, default: nil, readonly: false)
|
|
133
|
-
# Add field to the last declared scope by using ScopeBuilder within @schema.scope
|
|
134
|
-
# This method will be called inside Schema.scope block via Drawer
|
|
135
|
-
@schema.scopes[@scope_name].field(name, type: type, default: default, readonly: readonly)
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
data/lib/standard_config.rb
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
require "standard_config/config"
|
|
2
|
-
require "standard_config/config_provider"
|
|
3
|
-
require "standard_config/manager"
|
|
4
|
-
require "standard_config/schema"
|
|
5
|
-
|
|
6
|
-
require "concurrent/delay"
|
|
7
|
-
|
|
8
|
-
module StandardConfig
|
|
9
|
-
SCHEMA = Concurrent::Delay.new { Schema.new }
|
|
10
|
-
MANAGER = Concurrent::Delay.new { Manager.new(SCHEMA.value) }
|
|
11
|
-
|
|
12
|
-
class << self
|
|
13
|
-
def schema
|
|
14
|
-
SCHEMA.value
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def configure(&block)
|
|
18
|
-
if block_given? && block.arity.zero? && !config.registered?(:base)
|
|
19
|
-
config.register(:base, block)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
yield config if block_given?
|
|
23
|
-
|
|
24
|
-
config
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def config
|
|
28
|
-
MANAGER.value
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
def create_default_config
|
|
34
|
-
require "ostruct"
|
|
35
|
-
static_config = OpenStruct.new
|
|
36
|
-
base_scope = schema.scopes[:base]
|
|
37
|
-
if base_scope
|
|
38
|
-
base_scope.fields.each do |field_name, field_def|
|
|
39
|
-
static_config.send("#{field_name}=", field_def.default_value)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
static_config
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|