@voxgig/sdkgen 4.5.3 → 4.7.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.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/action/check.js +15 -0
- package/dist/action/check.js.map +1 -1
- package/dist/cmp/AgentGuide.js +1 -1
- package/dist/cmp/AgentGuide.js.map +1 -1
- package/dist/cmp/AgentGuideContent.d.ts +1 -1
- package/dist/cmp/AgentGuideContent.js +8 -2
- package/dist/cmp/AgentGuideContent.js.map +1 -1
- package/dist/cmp/Feature.js +22 -0
- package/dist/cmp/Feature.js.map +1 -1
- package/dist/cmp/FeatureDocs.d.ts +1 -1
- package/dist/cmp/FeatureDocs.js +6 -1
- package/dist/cmp/FeatureDocs.js.map +1 -1
- package/dist/cmp/ReadmeExplanation.js +3 -1
- package/dist/cmp/ReadmeExplanation.js.map +1 -1
- package/dist/cmp/ReadmeFeatures.js +1 -1
- package/dist/cmp/ReadmeFeatures.js.map +1 -1
- package/dist/cmp/ReadmeRefFeatures.js +1 -1
- package/dist/cmp/ReadmeRefFeatures.js.map +1 -1
- package/dist/cmp/TestControl.d.ts +4 -0
- package/dist/cmp/TestControl.js +51 -0
- package/dist/cmp/TestControl.js.map +1 -0
- package/dist/helpers/applicability.d.ts +6 -0
- package/dist/helpers/applicability.js +103 -0
- package/dist/helpers/applicability.js.map +1 -0
- package/dist/helpers/collectDeps.js +4 -2
- package/dist/helpers/collectDeps.js.map +1 -1
- package/dist/helpers/featureSource.d.ts +3 -1
- package/dist/helpers/featureSource.js +105 -0
- package/dist/helpers/featureSource.js.map +1 -1
- package/dist/helpers/serverVars.d.ts +2 -1
- package/dist/helpers/serverVars.js +17 -0
- package/dist/helpers/serverVars.js.map +1 -1
- package/dist/sdkgen.d.ts +5 -3
- package/dist/sdkgen.js +16 -4
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +59 -1
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.aon +97 -0
- package/package.json +1 -1
- package/project/.sdk/model/feature/feature-index.aon +1 -0
- package/project/.sdk/model/feature/secrets.aon +200 -0
- package/project/.sdk/model/target/ts.aon +8 -1
- package/project/.sdk/src/cmp/c/Config_c.ts +5 -1
- package/project/.sdk/src/cmp/cpp/Config_cpp.ts +5 -1
- package/project/.sdk/src/cmp/cpp/Main_cpp.ts +2 -1
- package/project/.sdk/src/cmp/cpp/Test_cpp.ts +5 -1
- package/project/.sdk/src/cmp/csharp/Config_csharp.ts +5 -1
- package/project/.sdk/src/cmp/csharp/Main_csharp.ts +2 -1
- package/project/.sdk/src/cmp/csharp/TestDirect_csharp.ts +30 -6
- package/project/.sdk/src/cmp/csharp/TestEntity_csharp.ts +29 -5
- package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -1
- package/project/.sdk/src/cmp/dart/Config_dart.ts +5 -1
- package/project/.sdk/src/cmp/dart/Main_dart.ts +2 -1
- package/project/.sdk/src/cmp/dart/TestDirect_dart.ts +23 -6
- package/project/.sdk/src/cmp/dart/TestEntity_dart.ts +28 -7
- package/project/.sdk/src/cmp/dart/Test_dart.ts +5 -1
- package/project/.sdk/src/cmp/elixir/Config_elixir.ts +5 -1
- package/project/.sdk/src/cmp/elixir/Main_elixir.ts +5 -1
- package/project/.sdk/src/cmp/go/Config_go.ts +5 -1
- package/project/.sdk/src/cmp/go/Main_go.ts +8 -3
- package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +16 -3
- package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +16 -3
- package/project/.sdk/src/cmp/go/TestDirect_go.ts +28 -4
- package/project/.sdk/src/cmp/go/TestEntity_go.ts +43 -10
- package/project/.sdk/src/cmp/go/Test_go.ts +5 -1
- package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +5 -5
- package/project/.sdk/src/cmp/java/Config_java.ts +22 -1
- package/project/.sdk/src/cmp/java/Main_java.ts +7 -2
- package/project/.sdk/src/cmp/java/ReadmeQuick_java.ts +17 -3
- package/project/.sdk/src/cmp/java/ReadmeTopQuick_java.ts +17 -3
- package/project/.sdk/src/cmp/java/TestDirect_java.ts +23 -4
- package/project/.sdk/src/cmp/java/TestEntity_java.ts +29 -4
- package/project/.sdk/src/cmp/java/Test_java.ts +5 -1
- package/project/.sdk/src/cmp/js/Config_js.ts +5 -1
- package/project/.sdk/src/cmp/js/Main_js.ts +7 -2
- package/project/.sdk/src/cmp/js/Package_js.ts +4 -1
- package/project/.sdk/src/cmp/js/TestDirect_js.ts +30 -5
- package/project/.sdk/src/cmp/js/TestEntity_js.ts +35 -5
- package/project/.sdk/src/cmp/js/Test_js.ts +5 -1
- package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +7 -1
- package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +7 -1
- package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +5 -1
- package/project/.sdk/src/cmp/kotlin/Main_kotlin.ts +2 -1
- package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +5 -1
- package/project/.sdk/src/cmp/lean/Config_lean.ts +5 -1
- package/project/.sdk/src/cmp/lua/Config_lua.ts +5 -1
- package/project/.sdk/src/cmp/lua/Main_lua.ts +7 -2
- package/project/.sdk/src/cmp/lua/Package_lua.ts +4 -1
- package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +28 -4
- package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +24 -4
- package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -1
- package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +5 -1
- package/project/.sdk/src/cmp/ocaml/Gitignore_ocaml.ts +1 -0
- package/project/.sdk/src/cmp/perl/Main_perl.ts +7 -2
- package/project/.sdk/src/cmp/perl/TestDirect_perl.ts +25 -4
- package/project/.sdk/src/cmp/perl/TestEntity_perl.ts +24 -4
- package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -1
- package/project/.sdk/src/cmp/perl/utility_perl.ts +10 -0
- package/project/.sdk/src/cmp/php/Config_php.ts +5 -1
- package/project/.sdk/src/cmp/php/Main_php.ts +7 -2
- package/project/.sdk/src/cmp/php/TestDirect_php.ts +24 -5
- package/project/.sdk/src/cmp/php/TestEntity_php.ts +28 -4
- package/project/.sdk/src/cmp/php/Test_php.ts +5 -1
- package/project/.sdk/src/cmp/py/Config_py.ts +5 -1
- package/project/.sdk/src/cmp/py/Main_py.ts +7 -2
- package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +16 -3
- package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +16 -3
- package/project/.sdk/src/cmp/py/TestDirect_py.ts +24 -5
- package/project/.sdk/src/cmp/py/TestEntity_py.ts +23 -3
- package/project/.sdk/src/cmp/py/Test_py.ts +5 -1
- package/project/.sdk/src/cmp/rb/Config_rb.ts +5 -1
- package/project/.sdk/src/cmp/rb/Main_rb.ts +7 -2
- package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +24 -5
- package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +24 -4
- package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -1
- package/project/.sdk/src/cmp/rb/utility_rb.ts +3 -1
- package/project/.sdk/src/cmp/rust/Config_rust.ts +5 -1
- package/project/.sdk/src/cmp/rust/Main_rust.ts +7 -2
- package/project/.sdk/src/cmp/rust/TestDirect_rust.ts +31 -4
- package/project/.sdk/src/cmp/rust/TestEntity_rust.ts +37 -4
- package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -1
- package/project/.sdk/src/cmp/scala/Config_scala.ts +5 -1
- package/project/.sdk/src/cmp/scala/Main_scala.ts +7 -2
- package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -1
- package/project/.sdk/src/cmp/swift/Config_swift.ts +5 -1
- package/project/.sdk/src/cmp/ts/Config_ts.ts +55 -9
- package/project/.sdk/src/cmp/ts/Main_ts.ts +67 -3
- package/project/.sdk/src/cmp/ts/Package_ts.ts +4 -1
- package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +15 -3
- package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +18 -5
- package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +19 -5
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +75 -8
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +36 -5
- package/project/.sdk/src/cmp/ts/Test_ts.ts +5 -1
- package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +9 -2
- package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +9 -2
- package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +7 -0
- package/project/.sdk/src/cmp/zig/Config_zig.ts +5 -1
- package/project/.sdk/tm/c/utility/make_options.c +22 -0
- package/project/.sdk/tm/clojure/src/sdk/core.clj +18 -1
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +23 -0
- package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +15 -0
- package/project/.sdk/tm/csharp/test/Runner.cs +45 -0
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +23 -0
- package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +22 -0
- package/project/.sdk/tm/dart/test/utility.dart +39 -0
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +19 -0
- package/project/.sdk/tm/go/core/context.go +1 -1
- package/project/.sdk/tm/go/feature/cost_feature.go +1 -1
- package/project/.sdk/tm/go/feature/paging_feature.go +5 -5
- package/project/.sdk/tm/go/feature/test_feature.go +12 -5
- package/project/.sdk/tm/go/test/custom_utility_test.go +15 -0
- package/project/.sdk/tm/go/test/feature_corpus_test.go +7 -0
- package/project/.sdk/tm/go/test/runner_test.go +46 -0
- package/project/.sdk/tm/go/test/struct_runner_test.go +7 -7
- package/project/.sdk/tm/go/test/struct_utility_test.go +63 -27
- package/project/.sdk/tm/go/utility/fetcher.go +2 -2
- package/project/.sdk/tm/go/utility/make_options.go +24 -5
- package/project/.sdk/tm/go/utility/make_point.go +1 -1
- package/project/.sdk/tm/go/utility/make_spec.go +17 -1
- package/project/.sdk/tm/go/utility/prepare_auth.go +1 -1
- package/project/.sdk/tm/go/utility/struct/voxgigstruct.go +370 -307
- package/project/.sdk/tm/go/utility/transform_request.go +29 -1
- package/project/.sdk/tm/go/utility/transform_response.go +20 -1
- package/project/.sdk/tm/java/test/CustomUtilityTest.java +10 -0
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +7 -0
- package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +6 -0
- package/project/.sdk/tm/java/test/RunnerSupport.java +43 -0
- package/project/.sdk/tm/java/utility/MakeOptions.java +84 -1
- package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +24 -0
- package/project/.sdk/tm/js/src/utility/StructUtility.js +2076 -1837
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +9 -0
- package/project/.sdk/tm/js/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/js/test/utility.js +91 -1
- package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +21 -0
- package/project/.sdk/tm/lean/src/SdkUtility.lean +12 -1
- package/project/.sdk/tm/lua/test/runner.lua +40 -0
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +11 -0
- package/project/.sdk/tm/ocaml/test/harness.ml +14 -4
- package/project/.sdk/tm/perl/t/runner.pm +30 -0
- package/project/.sdk/tm/perl/utility/make_options.pm +18 -0
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +10 -1
- package/project/.sdk/tm/php/test/Runner.php +36 -0
- package/project/.sdk/tm/php/utility/MakeOptions.php +24 -0
- package/project/.sdk/tm/py/pkg/utility/make_options.py +20 -0
- package/project/.sdk/tm/py/test/runner.py +32 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +10 -1
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +9 -1
- package/project/.sdk/tm/rb/test/runner.rb +25 -0
- package/project/.sdk/tm/rb/utility/make_options.rb +17 -0
- package/project/.sdk/tm/rust/tests/common/mod.rs +32 -0
- package/project/.sdk/tm/rust/utility/make_options.rs +26 -1
- package/project/.sdk/tm/scala/utility/Make.scala +17 -0
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -0
- package/project/.sdk/tm/ts/src/feature/secrets/SecretsFeature.ts +419 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +387 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +175 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +51 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/Registry.ts +90 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/addr.ts +48 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/aws.ts +219 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/azuresecrets.ts +144 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/boru.ts +113 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/doppler.ts +77 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/dotenv.ts +51 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/env.ts +31 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/file.ts +54 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/gcpsecrets.ts +118 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/hashicorp.ts +159 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/http.ts +66 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/infisical.ts +107 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/memory.ts +33 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/onepassword.ts +120 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/support.ts +176 -0
- package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +18 -0
- package/project/.sdk/tm/ts/src/utility/StructUtility.ts +1321 -710
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +9 -0
- package/project/.sdk/tm/ts/test/feature/secrets/Secrets.test.ts +764 -0
- package/project/.sdk/tm/ts/test/omni.test.ts +84 -0
- package/project/.sdk/tm/ts/test/omni.ts +86 -0
- package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +6 -2
- package/project/.sdk/tm/ts/test/utility/StructUtility.test.ts +2 -2
- package/project/.sdk/tm/ts/test/utility.ts +122 -0
- package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +694 -0
- package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +244 -0
- package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +188 -0
- package/project/.sdk/tm/ts/test/vendor/omni/index.ts +38 -0
- package/project/.sdk/tm/zig/core/utility.zig +24 -1
- package/project/sdkgen-package.json +2 -1
- package/src/action/check.ts +18 -0
- package/src/cmp/AgentGuide.ts +1 -1
- package/src/cmp/AgentGuideContent.ts +9 -2
- package/src/cmp/Feature.ts +24 -0
- package/src/cmp/FeatureDocs.ts +7 -1
- package/src/cmp/ReadmeExplanation.ts +4 -1
- package/src/cmp/ReadmeFeatures.ts +1 -1
- package/src/cmp/ReadmeRefFeatures.ts +1 -1
- package/src/cmp/TestControl.ts +64 -0
- package/src/helpers/applicability.ts +123 -0
- package/src/helpers/collectDeps.ts +5 -1
- package/src/helpers/featureSource.ts +130 -0
- package/src/helpers/serverVars.ts +19 -0
- package/src/sdkgen.ts +16 -1
- package/src/utility.ts +71 -1
- package/project/.sdk/tm/ts/test/runner.ts +0 -425
|
@@ -40,9 +40,23 @@ module ProjectNameUtilities
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# `auth: nil` is the documented way to disable auth outright, and
|
|
44
|
+
# prepare_auth honours it before it ever reads the apikey. It cannot
|
|
45
|
+
# survive validate: depending on the struct port a stored null is either
|
|
46
|
+
# REPLACED by the optspec default - transmitting the credential the
|
|
47
|
+
# caller withheld - or REJECTED outright. Withhold the key for validate,
|
|
48
|
+
# then put the nil back. Same fix as ts/js/go make_options.
|
|
49
|
+
#
|
|
50
|
+
# Suppliedness cannot be recovered after validate, hence here, and it
|
|
51
|
+
# must tell an ABSENT auth from a present nil: only the latter is a
|
|
52
|
+
# suppression.
|
|
53
|
+
authsuppressed = options.is_a?(Hash) && options.key?('auth') && options['auth'].nil?
|
|
54
|
+
|
|
43
55
|
opts = VoxgigStruct.clone(options)
|
|
44
56
|
opts = {} unless opts.is_a?(Hash)
|
|
45
57
|
|
|
58
|
+
opts.delete('auth') if authsuppressed
|
|
59
|
+
|
|
46
60
|
# Feature add-order. options["feature"] may be given as an ordered ARRAY of
|
|
47
61
|
# { "name" => ..., "active" => ..., ... } entries (the array position IS the
|
|
48
62
|
# order in which features are added), or as a { "name" => {opts} } map.
|
|
@@ -109,6 +123,9 @@ module ProjectNameUtilities
|
|
|
109
123
|
validated = VoxgigStruct.validate(merged, optspec)
|
|
110
124
|
opts = validated.is_a?(Hash) ? validated : {}
|
|
111
125
|
|
|
126
|
+
# Restore the suppression the optspec default would otherwise erase.
|
|
127
|
+
opts['auth'] = nil if authsuppressed
|
|
128
|
+
|
|
112
129
|
# Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
|
|
113
130
|
# Every placeholder must resolve to a non-empty value: from
|
|
114
131
|
# options["server"] (user), else the config default. A placeholder that
|
|
@@ -93,6 +93,38 @@ pub fn is_control_skipped(kind: &str, name: &str, mode: &str) -> (bool, String)
|
|
|
93
93
|
(false, String::new())
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/// Extra SDK options every LIVE client is constructed with, read from
|
|
97
|
+
/// sdk-test-control.json `test.client.options`.
|
|
98
|
+
///
|
|
99
|
+
/// The generated live client knows two things: the base URL (from the spec)
|
|
100
|
+
/// and the credential (from the environment). Everything else about how a
|
|
101
|
+
/// particular API wants to be talked to - which features to switch on, and
|
|
102
|
+
/// with what settings - is a property of THAT API, known to the project and
|
|
103
|
+
/// to nothing in the toolchain.
|
|
104
|
+
///
|
|
105
|
+
/// Merged UNDER the generated fields, so the suite's own base/apikey/server
|
|
106
|
+
/// values win: this ADDS to the live client, it does not redirect it.
|
|
107
|
+
///
|
|
108
|
+
/// Reserved fields are stripped HERE rather than at each merge site: the
|
|
109
|
+
/// generated map only names a field when the model calls for one, so a
|
|
110
|
+
/// "base" in this block would face no competing value and would silently
|
|
111
|
+
/// redirect the whole suite - credential included - to another host.
|
|
112
|
+
pub const LIVE_RESERVED: [&str; 6] =
|
|
113
|
+
["base", "prefix", "suffix", "server", "apikey", "secret"];
|
|
114
|
+
|
|
115
|
+
pub fn live_client_options() -> Value {
|
|
116
|
+
let ctrl = load_test_control();
|
|
117
|
+
let out = Value::empty_map();
|
|
118
|
+
if let Value::Map(m) = get_spec(&ctrl, &["test", "client", "options"]) {
|
|
119
|
+
for (k, v) in m.borrow().iter() {
|
|
120
|
+
if !LIVE_RESERVED.contains(&k.as_str()) {
|
|
121
|
+
setp(&out, k, v.clone());
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
out
|
|
126
|
+
}
|
|
127
|
+
|
|
96
128
|
/// Configured per-test live delay in ms; default 500.
|
|
97
129
|
pub fn live_delay_ms() -> i64 {
|
|
98
130
|
let ctrl = load_test_control();
|
|
@@ -23,8 +23,28 @@ pub fn make_options_util(ctx: &Rc<Context>) -> Value {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// `auth: null` is the documented way to disable auth outright, and
|
|
27
|
+
// prepare_auth honours it before it ever reads the apikey. It cannot
|
|
28
|
+
// survive validate: depending on the struct port a stored null is either
|
|
29
|
+
// REPLACED by the optspec default - transmitting the credential the
|
|
30
|
+
// caller withheld - or REJECTED outright. Withhold the key for validate,
|
|
31
|
+
// then put the null back. Same fix as ts/js/go make_options.
|
|
32
|
+
//
|
|
33
|
+
// Read the map DIRECTLY rather than through get_prop: get_prop applies
|
|
34
|
+
// the Group A rule and returns the alt for a stored null, so it cannot
|
|
35
|
+
// tell an absent auth from a suppressed one - and only the latter is a
|
|
36
|
+
// suppression.
|
|
37
|
+
let auth_suppressed = match &options {
|
|
38
|
+
Value::Map(m) => matches!(m.borrow().get("auth"), Some(Value::Null)),
|
|
39
|
+
_ => false,
|
|
40
|
+
};
|
|
41
|
+
|
|
26
42
|
let mut opts = vs::clone(&options);
|
|
27
43
|
|
|
44
|
+
if auth_suppressed {
|
|
45
|
+
opts = vs::del_prop(opts, &Value::str("auth"));
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
// Feature add-order. `options.feature` may be an ordered List of
|
|
29
49
|
// { name, active, ...opts } entries (the List position IS the order in
|
|
30
50
|
// which features are added), or a { name: {opts} } map. Normalize a List
|
|
@@ -58,7 +78,7 @@ pub fn make_options_util(ctx: &Rc<Context>) -> Value {
|
|
|
58
78
|
("base", Value::str("http://localhost:8000")),
|
|
59
79
|
("prefix", Value::str("")),
|
|
60
80
|
("suffix", Value::str("")),
|
|
61
|
-
("auth", jo(vec![("prefix", Value::str(""))])),
|
|
81
|
+
("auth", jo(vec![("prefix", Value::str("")), ("basic", Value::Bool(false))])),
|
|
62
82
|
("headers", jo(vec![("`$CHILD`", Value::str("`$STRING`"))])),
|
|
63
83
|
(
|
|
64
84
|
"allow",
|
|
@@ -120,6 +140,11 @@ pub fn make_options_util(ctx: &Rc<Context>) -> Value {
|
|
|
120
140
|
}
|
|
121
141
|
}
|
|
122
142
|
|
|
143
|
+
// Restore the suppression the optspec default would otherwise erase.
|
|
144
|
+
if auth_suppressed {
|
|
145
|
+
setp(&opts, "auth", Value::Null);
|
|
146
|
+
}
|
|
147
|
+
|
|
123
148
|
// Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
|
|
124
149
|
// Every placeholder must resolve to a non-empty value: from options.server
|
|
125
150
|
// (user), else the Config default. A placeholder that resolves to "" is a
|
|
@@ -254,8 +254,22 @@ object MakeOptions {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// `auth: null` is the documented way to suppress auth outright, and
|
|
258
|
+
// prepareAuth honours it before it ever reads the apikey. It cannot survive
|
|
259
|
+
// validate: a stored null reads as "no value", so the optspec `auth`
|
|
260
|
+
// default fires and the suppression becomes "use the default auth" -
|
|
261
|
+
// transmitting the credential the caller withheld. Withhold the key for
|
|
262
|
+
// validate, then put the null back. Same fix as ts/js/go/java makeOptions.
|
|
263
|
+
//
|
|
264
|
+
// Suppliedness cannot be recovered after validate, hence here, and it must
|
|
265
|
+
// tell an ABSENT auth from a present null: containsKey rather than a get()
|
|
266
|
+
// null check, which cannot distinguish them.
|
|
267
|
+
val authSuppressed = options.containsKey("auth") && null == options.get("auth")
|
|
268
|
+
|
|
257
269
|
var opts = Struct.clone(options).asInstanceOf[JMap[String, Object]]
|
|
258
270
|
|
|
271
|
+
if (authSuppressed) opts.remove("auth")
|
|
272
|
+
|
|
259
273
|
// Feature add-order. options.feature may be given as an ordered LIST of
|
|
260
274
|
// { name, active, ...opts } entries (the list position IS the order in
|
|
261
275
|
// which features are added), or as a { name: {opts} } map. Normalize a
|
|
@@ -330,6 +344,9 @@ object MakeOptions {
|
|
|
330
344
|
val validated = Struct.validate(merged, optspec, vopts)
|
|
331
345
|
opts = validated.asInstanceOf[JMap[String, Object]]
|
|
332
346
|
|
|
347
|
+
// Restore the suppression the optspec default would otherwise erase.
|
|
348
|
+
if (authSuppressed) opts.put("auth", null)
|
|
349
|
+
|
|
333
350
|
// Restore system.fetch.
|
|
334
351
|
if (sysFetch != null) {
|
|
335
352
|
val sys = Helpers.toMapAny(opts.get("system"))
|
|
@@ -85,8 +85,24 @@ func makeOptionsUtil(_ ctx: Context) -> VMap {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// `auth: .null` is the documented way to disable auth outright, and
|
|
89
|
+
// prepareAuth honours it before it ever reads the apikey. It cannot survive
|
|
90
|
+
// validate: depending on the struct port a stored null is either REPLACED
|
|
91
|
+
// by the optspec default - transmitting the credential the caller withheld
|
|
92
|
+
// - or REJECTED outright. Withhold the key for validate, then put the null
|
|
93
|
+
// back. Same fix as ts/js/go makeOptions.
|
|
94
|
+
//
|
|
95
|
+
// `isNull` rather than `isNil`: the latter is also true for Noval, so it
|
|
96
|
+
// cannot tell an ABSENT auth from a suppressed one, and only the latter is
|
|
97
|
+
// a suppression.
|
|
98
|
+
let authSuppressed = options.entries["auth"]?.isNull ?? false
|
|
99
|
+
|
|
88
100
|
let opts = clone(.map(options)).asMap ?? VMap()
|
|
89
101
|
|
|
102
|
+
if authSuppressed {
|
|
103
|
+
opts.entries.removeValue(forKey: "auth")
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
// Feature add-order. options.feature may be given as an ordered LIST of
|
|
91
107
|
// { name, active, ...opts } entries (the list position IS the order in which
|
|
92
108
|
// features are added), or as a { name: {opts} } map. Normalize a list to a
|
|
@@ -124,6 +140,11 @@ func makeOptionsUtil(_ ctx: Context) -> VMap {
|
|
|
124
140
|
let validated = validate(merged, optspec)
|
|
125
141
|
let result = validated.asMap ?? VMap()
|
|
126
142
|
|
|
143
|
+
// Restore the suppression the optspec default would otherwise erase.
|
|
144
|
+
if authSuppressed {
|
|
145
|
+
result.entries["auth"] = .null
|
|
146
|
+
}
|
|
147
|
+
|
|
127
148
|
// Restore system.fetch.
|
|
128
149
|
if !isNil(sysFetch) {
|
|
129
150
|
if let sm = gp(result, "system").asMap {
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import type { Context, FeatureOptions } from '../../types'
|
|
2
|
+
|
|
3
|
+
import { BaseFeature } from '../base/BaseFeature'
|
|
4
|
+
import { Sekreto, envkey } from './sekreto'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// Secret access via a vendored @voxgig/sekreto provider chain, and the
|
|
8
|
+
// access-token exchange some APIs require on top of it.
|
|
9
|
+
//
|
|
10
|
+
// The SDK's `apikey` option keeps exactly its old meaning: an explicit
|
|
11
|
+
// credential given in code. This feature makes it ONE SOURCE among several
|
|
12
|
+
// rather than the only one: when active, the apikey is resolved through a
|
|
13
|
+
// sekreto chain in which the explicit option (when set) is the FIRST
|
|
14
|
+
// provider — a `memory` store named `options` — so an explicit value always
|
|
15
|
+
// wins, by sekreto's own first-hit rule rather than by special-case logic.
|
|
16
|
+
// When the option is unset, the remaining providers (env, dotenv, a vault)
|
|
17
|
+
// are asked in order, and moving a credential from code to a vault becomes
|
|
18
|
+
// a configuration change.
|
|
19
|
+
//
|
|
20
|
+
// SOME APIs will not take a long-lived credential at all. What the chain
|
|
21
|
+
// resolves is then a REFRESH token, which buys a short-lived ACCESS token
|
|
22
|
+
// from a token endpoint; the access token is what every request carries,
|
|
23
|
+
// and it expires. `exchange.active` turns that round trip on: the resolved
|
|
24
|
+
// secret is POSTed to `exchange.path` (relative to options.base), the
|
|
25
|
+
// access token from the response is written to options.apikey, and a
|
|
26
|
+
// response in `exchange.statuses` (401) buys another and retries once.
|
|
27
|
+
//
|
|
28
|
+
// That last part is why this feature wraps the transport. Expiry is only
|
|
29
|
+
// ever discovered from a RESPONSE, and the transport is the one place a
|
|
30
|
+
// response can be seen before the operation pipeline turns it into a
|
|
31
|
+
// result. With the exchange off, nothing is wrapped.
|
|
32
|
+
//
|
|
33
|
+
// Resolution is ASYNC (providers may do IO), but the auth header is built
|
|
34
|
+
// by the synchronous prepareAuth inside makeSpec. The bridge is the
|
|
35
|
+
// feature hook pipeline: every entity op awaits featureHook('PreSpec')
|
|
36
|
+
// before calling makeSpec, so the PreSpec hook below resolves the secret
|
|
37
|
+
// once and writes it into the live options where prepareAuth already
|
|
38
|
+
// looks. prepareAuth itself is untouched. SolardemoSDK.prepare() bypasses
|
|
39
|
+
// feature hooks entirely, so it awaits client._secrets.resolve() itself.
|
|
40
|
+
class SecretsFeature extends BaseFeature {
|
|
41
|
+
version = '0.1.0'
|
|
42
|
+
name = 'secrets'
|
|
43
|
+
|
|
44
|
+
_client: any
|
|
45
|
+
_sekreto?: Sekreto
|
|
46
|
+
_secretname: string = 'apikey'
|
|
47
|
+
_resolving?: Promise<void>
|
|
48
|
+
_cache: boolean = true
|
|
49
|
+
|
|
50
|
+
// Exchange state. `_exchange` is the resolved config (null when off),
|
|
51
|
+
// `_refresh` the credential the chain gave us, and `_buying` the single
|
|
52
|
+
// in-flight token purchase concurrent callers share.
|
|
53
|
+
_exchange: any = null
|
|
54
|
+
_refresh?: string
|
|
55
|
+
_buying?: Promise<string>
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// Sync by contract (the constructor cannot await): build the chain only,
|
|
59
|
+
// never look anything up here.
|
|
60
|
+
init(ctx: Context, fopts: FeatureOptions): void {
|
|
61
|
+
const client = ctx.client
|
|
62
|
+
const options = ctx.options
|
|
63
|
+
|
|
64
|
+
this._client = client
|
|
65
|
+
this._secretname = 'string' === typeof (fopts as any).name &&
|
|
66
|
+
'' !== (fopts as any).name ? (fopts as any).name : 'apikey'
|
|
67
|
+
|
|
68
|
+
// Exchange config, normalised once. Null when off, so every later
|
|
69
|
+
// decision is a null check rather than a repeated `true === ...active`.
|
|
70
|
+
const xopts = (fopts as any).exchange
|
|
71
|
+
this._exchange = (null != xopts && true === xopts.active) ? {
|
|
72
|
+
path: 'string' === typeof xopts.path ? xopts.path : 'auth/token',
|
|
73
|
+
method: 'string' === typeof xopts.method ? xopts.method : 'POST',
|
|
74
|
+
request: 'string' === typeof xopts.request ? xopts.request : 'refresh_token',
|
|
75
|
+
response: 'string' === typeof xopts.response ? xopts.response : 'access_token',
|
|
76
|
+
statuses: Array.isArray(xopts.statuses) ? xopts.statuses : [401],
|
|
77
|
+
retries: 'number' === typeof xopts.retries ? xopts.retries : 1,
|
|
78
|
+
} : null
|
|
79
|
+
|
|
80
|
+
const providers: any[] = []
|
|
81
|
+
|
|
82
|
+
// The explicit credential, when set, is the first store in the chain.
|
|
83
|
+
//
|
|
84
|
+
// WHICH option that is depends on the exchange. Without one, the secret
|
|
85
|
+
// being resolved IS the credential the transport sends, so `apikey` is
|
|
86
|
+
// it. With one, the secret is a REFRESH token and `apikey` means the
|
|
87
|
+
// opposite thing — an access token the caller already holds — so the
|
|
88
|
+
// explicit seat belongs to `exchange.refresh`, and apikey is left alone
|
|
89
|
+
// to serve as the starting access token (see resolve below).
|
|
90
|
+
const explicit = null == this._exchange ?
|
|
91
|
+
options.apikey :
|
|
92
|
+
(null == xopts ? undefined : xopts.refresh)
|
|
93
|
+
|
|
94
|
+
if ('string' === typeof explicit && '' !== explicit) {
|
|
95
|
+
providers.push({
|
|
96
|
+
kind: 'memory',
|
|
97
|
+
name: 'options',
|
|
98
|
+
values: { [envkey(this._secretname)]: explicit },
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const p of ((fopts as any).providers || [])) {
|
|
103
|
+
providers.push(p)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this._cache = false !== (fopts as any).cache
|
|
107
|
+
|
|
108
|
+
this._sekreto = new Sekreto({
|
|
109
|
+
providers,
|
|
110
|
+
cache: this._cache,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Seam for ProjectNameSDK.prepare() (no feature hooks on that path)
|
|
114
|
+
// and for the public secrets() accessor.
|
|
115
|
+
//
|
|
116
|
+
// Cast, because `_secrets` is emitted on the SDK class only when this
|
|
117
|
+
// feature is active, while this file is copied into the target tree by
|
|
118
|
+
// Main's blanket copy whether or not the model declares the feature.
|
|
119
|
+
// Typing the assignment would make the SDK fail to compile in exactly
|
|
120
|
+
// the case the feature is switched off.
|
|
121
|
+
;(client as any)._secrets = this
|
|
122
|
+
|
|
123
|
+
// Wrap the transport ONLY when there is an exchange to defend. A spent
|
|
124
|
+
// access token is discovered from the response, and this is the one
|
|
125
|
+
// place a response can be seen and the request tried again.
|
|
126
|
+
if (null != this._exchange) {
|
|
127
|
+
const self = this
|
|
128
|
+
const utility = ctx.utility
|
|
129
|
+
const inner = utility.fetcher
|
|
130
|
+
|
|
131
|
+
utility.fetcher = async function (ctx2: any, url: string, fetchdef: any) {
|
|
132
|
+
return self._withRefresh(ctx2, url, fetchdef, inner)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
// The LIVE Sekreto instance, for the SDK's secrets() accessor and for
|
|
139
|
+
// callers who want arbitrary secrets or redaction:
|
|
140
|
+
//
|
|
141
|
+
// await sdk.secrets().get('db.password')
|
|
142
|
+
// sdk.secrets().redact(logline)
|
|
143
|
+
//
|
|
144
|
+
// Public, so the accessor does not have to reach into a private field.
|
|
145
|
+
// Never a clone: sekreto holds provider state (caches, vault leases)
|
|
146
|
+
// that has to stay live to be worth anything.
|
|
147
|
+
sekreto(): any {
|
|
148
|
+
return this._sekreto
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
PreSpec(_ctx: Context) {
|
|
153
|
+
return this.resolve()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
// Resolve the apikey before the first request. Concurrent ops share the
|
|
158
|
+
// one IN-FLIGHT promise; a settled one is not reused unless caching is
|
|
159
|
+
// on. A provider ERROR (unreachable vault, bad creds) rejects and fails
|
|
160
|
+
// the op — sekreto's miss-vs-error rule: never fall through to an
|
|
161
|
+
// unauthenticated request because a store was broken.
|
|
162
|
+
//
|
|
163
|
+
// The promise is cleared on REJECTION, and after success when caching is
|
|
164
|
+
// off. Holding a settled promise forever would mean a transient vault
|
|
165
|
+
// outage poisoned the client permanently — every later operation failing
|
|
166
|
+
// with the original error long after the vault recovered — and it would
|
|
167
|
+
// make the documented `cache: false` a lie, since the chain would never
|
|
168
|
+
// be asked a second time.
|
|
169
|
+
resolve(): Promise<void> {
|
|
170
|
+
if (null == this._resolving) {
|
|
171
|
+
const inflight = this._resolveonce()
|
|
172
|
+
.then(
|
|
173
|
+
() => {
|
|
174
|
+
if (!this._cache) {
|
|
175
|
+
this._resolving = undefined
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
(err: any) => {
|
|
179
|
+
this._resolving = undefined
|
|
180
|
+
throw err
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
this._resolving = inflight
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return this._resolving
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
private async _resolveonce(): Promise<void> {
|
|
191
|
+
if (null == this._sekreto) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const found = await this._sekreto.try(this._secretname)
|
|
196
|
+
|
|
197
|
+
if (null == this._exchange) {
|
|
198
|
+
if (undefined !== found) {
|
|
199
|
+
// The same live-mutation seam TestFeature uses for the transport:
|
|
200
|
+
// prepareAuth reads client.options() (a clone of _options), so the
|
|
201
|
+
// resolved value lands where the sync auth path already looks.
|
|
202
|
+
this._client._options.apikey = found
|
|
203
|
+
}
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Exchanging: what the chain resolved is the REFRESH token, kept for
|
|
208
|
+
// every later purchase. A miss is not fatal here — an explicit
|
|
209
|
+
// `apikey` may already hold a usable access token, and the API is
|
|
210
|
+
// what gets to say whether it does.
|
|
211
|
+
this._refresh = undefined === found ? undefined : String(found)
|
|
212
|
+
|
|
213
|
+
const apikey = this._client._options.apikey
|
|
214
|
+
if ('string' === typeof apikey && '' !== apikey) {
|
|
215
|
+
// A starting access token was supplied. Spend it: if it is stale the
|
|
216
|
+
// API answers 401 and the transport wrapper buys another, which is
|
|
217
|
+
// the same path expiry takes anyway.
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this._client._options.apikey = await this._buy()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
// Buy an access token with the refresh token.
|
|
226
|
+
//
|
|
227
|
+
// Concurrent callers share the ONE in-flight purchase: a client running
|
|
228
|
+
// four operations at once must not open four token requests, and three of
|
|
229
|
+
// the four tokens would be wasted. The promise is cleared once settled,
|
|
230
|
+
// so the next expiry buys a fresh one rather than replaying this result.
|
|
231
|
+
private _buy(): Promise<string> {
|
|
232
|
+
// TEST MODE BUYS NOTHING.
|
|
233
|
+
//
|
|
234
|
+
// The test feature replaces the transport so that no request leaves the
|
|
235
|
+
// process; an exchange here would be the one HTTP call it could not
|
|
236
|
+
// stop, and it would need a live token endpoint for a suite whose whole
|
|
237
|
+
// point is not needing one. So test mode gets a deterministic,
|
|
238
|
+
// obviously-fake token instead — the same answer makeOptions gives a
|
|
239
|
+
// required server variable, for the same reason.
|
|
240
|
+
if ('live' !== this._client._mode) {
|
|
241
|
+
return Promise.resolve('test-' + this._exchange.response)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (null != this._buying) {
|
|
245
|
+
return this._buying
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const buying = this._buyonce()
|
|
249
|
+
.then(
|
|
250
|
+
(token: string) => {
|
|
251
|
+
this._buying = undefined
|
|
252
|
+
return token
|
|
253
|
+
},
|
|
254
|
+
(err: any) => {
|
|
255
|
+
this._buying = undefined
|
|
256
|
+
throw err
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
this._buying = buying
|
|
260
|
+
|
|
261
|
+
return buying
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
private async _buyonce(): Promise<string> {
|
|
266
|
+
const x = this._exchange
|
|
267
|
+
|
|
268
|
+
if (null == this._refresh || "" === this._refresh) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
"secrets: no refresh token: the provider chain has no '" +
|
|
271
|
+
this._secretname + "', and feature.secrets.exchange.refresh is unset")
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const options = this._client.options()
|
|
275
|
+
|
|
276
|
+
// The token endpoint is RELATIVE to the base, which already carries
|
|
277
|
+
// whatever account or tenant segment the server URL declares.
|
|
278
|
+
const base = String(options.base || "").replace(/\/+$/, "")
|
|
279
|
+
const url = base + "/" + String(x.path).replace(/^\/+/, "")
|
|
280
|
+
|
|
281
|
+
const fetch = options.system && options.system.fetch
|
|
282
|
+
|
|
283
|
+
if ("function" !== typeof fetch) {
|
|
284
|
+
throw new Error("secrets: no fetch implementation for the token exchange")
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Deliberately NOT the SDK transport. The transport is what this
|
|
288
|
+
// feature wraps, and sending the token request back through it would
|
|
289
|
+
// recurse on the first 401 — and would route the exchange through the
|
|
290
|
+
// test mock, which knows nothing about it.
|
|
291
|
+
const res = await fetch(url, {
|
|
292
|
+
method: x.method,
|
|
293
|
+
headers: { "content-type": "application/json" },
|
|
294
|
+
body: JSON.stringify({ [x.request]: this._refresh }),
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const status = null == res ? 0 : res.status
|
|
298
|
+
|
|
299
|
+
if (200 > status || 300 <= status) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
"secrets: token exchange failed: " + status + " from " + url)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const body = "function" === typeof res.json ? await res.json() : res.body
|
|
305
|
+
const token = null == body ? undefined : body[x.response]
|
|
306
|
+
|
|
307
|
+
if ("string" !== typeof token || "" === token) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
"secrets: token exchange returned no '" + x.response + "' field from " + url)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return token
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
// Buy a token and try the request again when the API says the current one
|
|
317
|
+
// is spent. Wraps the transport; installed only when exchanging.
|
|
318
|
+
//
|
|
319
|
+
// The retry rewrites the Authorization header IN PLACE on the fetchdef,
|
|
320
|
+
// because the header was built by the synchronous prepareAuth before this
|
|
321
|
+
// request left, and it carries the token that just failed. Rebuilt the
|
|
322
|
+
// way prepareAuth builds it, from the same options.auth.prefix, so the
|
|
323
|
+
// two cannot drift.
|
|
324
|
+
async _withRefresh(
|
|
325
|
+
this: any, ctx: any, url: string, fetchdef: any, inner: any
|
|
326
|
+
): Promise<any> {
|
|
327
|
+
const x = this._exchange
|
|
328
|
+
const max = null == x ? 0 : x.retries
|
|
329
|
+
|
|
330
|
+
// `auth: null` is the documented way to send NO credential, and
|
|
331
|
+
// prepareAuth honours it by removing the header. A refusal of a
|
|
332
|
+
// deliberately unauthenticated request is not an expired token and
|
|
333
|
+
// cannot be fixed by buying one — retrying would transmit exactly the
|
|
334
|
+
// credential the caller suppressed.
|
|
335
|
+
if (null == this._client.options().auth) {
|
|
336
|
+
return inner(ctx, url, fetchdef)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let attempt = 0
|
|
340
|
+
|
|
341
|
+
for (; ;) {
|
|
342
|
+
// The credential THIS attempt goes out with, captured before it
|
|
343
|
+
// leaves: it is what tells a stale refusal apart from a fresh one.
|
|
344
|
+
const used = this._client._options.apikey
|
|
345
|
+
|
|
346
|
+
const res = await inner(ctx, url, fetchdef)
|
|
347
|
+
|
|
348
|
+
if (attempt >= max || !this._spent(res)) {
|
|
349
|
+
return res
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Another request may have bought a token while this one was in
|
|
353
|
+
// flight. Concurrent 401s share the in-flight purchase, but
|
|
354
|
+
// STAGGERED ones do not: the first finishes and clears it, and the
|
|
355
|
+
// second would then buy again — a second exchange for a token that
|
|
356
|
+
// is already current, and on a provider that invalidates the
|
|
357
|
+
// previous credential on issuance, one that breaks the first
|
|
358
|
+
// request's own retry. So spend what is current before buying.
|
|
359
|
+
const current = this._client._options.apikey
|
|
360
|
+
let token: string
|
|
361
|
+
|
|
362
|
+
if ('string' === typeof current && '' !== current && current !== used) {
|
|
363
|
+
token = current
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
try {
|
|
367
|
+
token = await this._buy()
|
|
368
|
+
}
|
|
369
|
+
catch (err: any) {
|
|
370
|
+
// The purchase failed: answer with the API's own refusal rather
|
|
371
|
+
// than this one. The caller asked for data, and the 401 is the
|
|
372
|
+
// more useful of the two — the exchange error is a symptom.
|
|
373
|
+
return res
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this._client._options.apikey = token
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this._reauth(fetchdef, token)
|
|
380
|
+
|
|
381
|
+
attempt++
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
_spent(this: any, res: any): boolean {
|
|
387
|
+
if (null == res || res instanceof Error) {
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
const statuses = this._exchange.statuses
|
|
391
|
+
return statuses.indexOf(res.status) >= 0
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
_reauth(this: any, fetchdef: any, token: string) {
|
|
396
|
+
if (null == fetchdef || null == fetchdef.headers) {
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const auth = this._client.options().auth
|
|
401
|
+
|
|
402
|
+
// Suppressed auth means NO header, the same answer prepareAuth gives.
|
|
403
|
+
// Reached defensively - _withRefresh does not retry at all when auth
|
|
404
|
+
// is null - but this is the function that writes the credential, so
|
|
405
|
+
// it is where the rule has to hold.
|
|
406
|
+
if (null == auth) {
|
|
407
|
+
delete fetchdef.headers.authorization
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const prefix = auth.prefix
|
|
412
|
+
fetchdef.headers.authorization = prefix ? prefix + " " + token : token
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
export {
|
|
418
|
+
SecretsFeature
|
|
419
|
+
}
|