@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.
Files changed (248) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/action/check.js +15 -0
  3. package/dist/action/check.js.map +1 -1
  4. package/dist/cmp/AgentGuide.js +1 -1
  5. package/dist/cmp/AgentGuide.js.map +1 -1
  6. package/dist/cmp/AgentGuideContent.d.ts +1 -1
  7. package/dist/cmp/AgentGuideContent.js +8 -2
  8. package/dist/cmp/AgentGuideContent.js.map +1 -1
  9. package/dist/cmp/Feature.js +22 -0
  10. package/dist/cmp/Feature.js.map +1 -1
  11. package/dist/cmp/FeatureDocs.d.ts +1 -1
  12. package/dist/cmp/FeatureDocs.js +6 -1
  13. package/dist/cmp/FeatureDocs.js.map +1 -1
  14. package/dist/cmp/ReadmeExplanation.js +3 -1
  15. package/dist/cmp/ReadmeExplanation.js.map +1 -1
  16. package/dist/cmp/ReadmeFeatures.js +1 -1
  17. package/dist/cmp/ReadmeFeatures.js.map +1 -1
  18. package/dist/cmp/ReadmeRefFeatures.js +1 -1
  19. package/dist/cmp/ReadmeRefFeatures.js.map +1 -1
  20. package/dist/cmp/TestControl.d.ts +4 -0
  21. package/dist/cmp/TestControl.js +51 -0
  22. package/dist/cmp/TestControl.js.map +1 -0
  23. package/dist/helpers/applicability.d.ts +6 -0
  24. package/dist/helpers/applicability.js +103 -0
  25. package/dist/helpers/applicability.js.map +1 -0
  26. package/dist/helpers/collectDeps.js +4 -2
  27. package/dist/helpers/collectDeps.js.map +1 -1
  28. package/dist/helpers/featureSource.d.ts +3 -1
  29. package/dist/helpers/featureSource.js +105 -0
  30. package/dist/helpers/featureSource.js.map +1 -1
  31. package/dist/helpers/serverVars.d.ts +2 -1
  32. package/dist/helpers/serverVars.js +17 -0
  33. package/dist/helpers/serverVars.js.map +1 -1
  34. package/dist/sdkgen.d.ts +5 -3
  35. package/dist/sdkgen.js +16 -4
  36. package/dist/sdkgen.js.map +1 -1
  37. package/dist/tsconfig.tsbuildinfo +1 -1
  38. package/dist/utility.d.ts +2 -1
  39. package/dist/utility.js +59 -1
  40. package/dist/utility.js.map +1 -1
  41. package/model/sdkgen.aon +97 -0
  42. package/package.json +1 -1
  43. package/project/.sdk/model/feature/feature-index.aon +1 -0
  44. package/project/.sdk/model/feature/secrets.aon +200 -0
  45. package/project/.sdk/model/target/ts.aon +8 -1
  46. package/project/.sdk/src/cmp/c/Config_c.ts +5 -1
  47. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +5 -1
  48. package/project/.sdk/src/cmp/cpp/Main_cpp.ts +2 -1
  49. package/project/.sdk/src/cmp/cpp/Test_cpp.ts +5 -1
  50. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +5 -1
  51. package/project/.sdk/src/cmp/csharp/Main_csharp.ts +2 -1
  52. package/project/.sdk/src/cmp/csharp/TestDirect_csharp.ts +30 -6
  53. package/project/.sdk/src/cmp/csharp/TestEntity_csharp.ts +29 -5
  54. package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -1
  55. package/project/.sdk/src/cmp/dart/Config_dart.ts +5 -1
  56. package/project/.sdk/src/cmp/dart/Main_dart.ts +2 -1
  57. package/project/.sdk/src/cmp/dart/TestDirect_dart.ts +23 -6
  58. package/project/.sdk/src/cmp/dart/TestEntity_dart.ts +28 -7
  59. package/project/.sdk/src/cmp/dart/Test_dart.ts +5 -1
  60. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +5 -1
  61. package/project/.sdk/src/cmp/elixir/Main_elixir.ts +5 -1
  62. package/project/.sdk/src/cmp/go/Config_go.ts +5 -1
  63. package/project/.sdk/src/cmp/go/Main_go.ts +8 -3
  64. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +16 -3
  65. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +16 -3
  66. package/project/.sdk/src/cmp/go/TestDirect_go.ts +28 -4
  67. package/project/.sdk/src/cmp/go/TestEntity_go.ts +43 -10
  68. package/project/.sdk/src/cmp/go/Test_go.ts +5 -1
  69. package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +5 -5
  70. package/project/.sdk/src/cmp/java/Config_java.ts +22 -1
  71. package/project/.sdk/src/cmp/java/Main_java.ts +7 -2
  72. package/project/.sdk/src/cmp/java/ReadmeQuick_java.ts +17 -3
  73. package/project/.sdk/src/cmp/java/ReadmeTopQuick_java.ts +17 -3
  74. package/project/.sdk/src/cmp/java/TestDirect_java.ts +23 -4
  75. package/project/.sdk/src/cmp/java/TestEntity_java.ts +29 -4
  76. package/project/.sdk/src/cmp/java/Test_java.ts +5 -1
  77. package/project/.sdk/src/cmp/js/Config_js.ts +5 -1
  78. package/project/.sdk/src/cmp/js/Main_js.ts +7 -2
  79. package/project/.sdk/src/cmp/js/Package_js.ts +4 -1
  80. package/project/.sdk/src/cmp/js/TestDirect_js.ts +30 -5
  81. package/project/.sdk/src/cmp/js/TestEntity_js.ts +35 -5
  82. package/project/.sdk/src/cmp/js/Test_js.ts +5 -1
  83. package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +7 -1
  84. package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +7 -1
  85. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +5 -1
  86. package/project/.sdk/src/cmp/kotlin/Main_kotlin.ts +2 -1
  87. package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +5 -1
  88. package/project/.sdk/src/cmp/lean/Config_lean.ts +5 -1
  89. package/project/.sdk/src/cmp/lua/Config_lua.ts +5 -1
  90. package/project/.sdk/src/cmp/lua/Main_lua.ts +7 -2
  91. package/project/.sdk/src/cmp/lua/Package_lua.ts +4 -1
  92. package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +28 -4
  93. package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +24 -4
  94. package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -1
  95. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +5 -1
  96. package/project/.sdk/src/cmp/ocaml/Gitignore_ocaml.ts +1 -0
  97. package/project/.sdk/src/cmp/perl/Main_perl.ts +7 -2
  98. package/project/.sdk/src/cmp/perl/TestDirect_perl.ts +25 -4
  99. package/project/.sdk/src/cmp/perl/TestEntity_perl.ts +24 -4
  100. package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -1
  101. package/project/.sdk/src/cmp/perl/utility_perl.ts +10 -0
  102. package/project/.sdk/src/cmp/php/Config_php.ts +5 -1
  103. package/project/.sdk/src/cmp/php/Main_php.ts +7 -2
  104. package/project/.sdk/src/cmp/php/TestDirect_php.ts +24 -5
  105. package/project/.sdk/src/cmp/php/TestEntity_php.ts +28 -4
  106. package/project/.sdk/src/cmp/php/Test_php.ts +5 -1
  107. package/project/.sdk/src/cmp/py/Config_py.ts +5 -1
  108. package/project/.sdk/src/cmp/py/Main_py.ts +7 -2
  109. package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +16 -3
  110. package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +16 -3
  111. package/project/.sdk/src/cmp/py/TestDirect_py.ts +24 -5
  112. package/project/.sdk/src/cmp/py/TestEntity_py.ts +23 -3
  113. package/project/.sdk/src/cmp/py/Test_py.ts +5 -1
  114. package/project/.sdk/src/cmp/rb/Config_rb.ts +5 -1
  115. package/project/.sdk/src/cmp/rb/Main_rb.ts +7 -2
  116. package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +24 -5
  117. package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +24 -4
  118. package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -1
  119. package/project/.sdk/src/cmp/rb/utility_rb.ts +3 -1
  120. package/project/.sdk/src/cmp/rust/Config_rust.ts +5 -1
  121. package/project/.sdk/src/cmp/rust/Main_rust.ts +7 -2
  122. package/project/.sdk/src/cmp/rust/TestDirect_rust.ts +31 -4
  123. package/project/.sdk/src/cmp/rust/TestEntity_rust.ts +37 -4
  124. package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -1
  125. package/project/.sdk/src/cmp/scala/Config_scala.ts +5 -1
  126. package/project/.sdk/src/cmp/scala/Main_scala.ts +7 -2
  127. package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -1
  128. package/project/.sdk/src/cmp/swift/Config_swift.ts +5 -1
  129. package/project/.sdk/src/cmp/ts/Config_ts.ts +55 -9
  130. package/project/.sdk/src/cmp/ts/Main_ts.ts +67 -3
  131. package/project/.sdk/src/cmp/ts/Package_ts.ts +4 -1
  132. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +15 -3
  133. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +18 -5
  134. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +19 -5
  135. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +75 -8
  136. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +36 -5
  137. package/project/.sdk/src/cmp/ts/Test_ts.ts +5 -1
  138. package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +9 -2
  139. package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +9 -2
  140. package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +7 -0
  141. package/project/.sdk/src/cmp/zig/Config_zig.ts +5 -1
  142. package/project/.sdk/tm/c/utility/make_options.c +22 -0
  143. package/project/.sdk/tm/clojure/src/sdk/core.clj +18 -1
  144. package/project/.sdk/tm/cpp/utility/pipeline.hpp +23 -0
  145. package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +15 -0
  146. package/project/.sdk/tm/csharp/test/Runner.cs +45 -0
  147. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +23 -0
  148. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +22 -0
  149. package/project/.sdk/tm/dart/test/utility.dart +39 -0
  150. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +19 -0
  151. package/project/.sdk/tm/go/core/context.go +1 -1
  152. package/project/.sdk/tm/go/feature/cost_feature.go +1 -1
  153. package/project/.sdk/tm/go/feature/paging_feature.go +5 -5
  154. package/project/.sdk/tm/go/feature/test_feature.go +12 -5
  155. package/project/.sdk/tm/go/test/custom_utility_test.go +15 -0
  156. package/project/.sdk/tm/go/test/feature_corpus_test.go +7 -0
  157. package/project/.sdk/tm/go/test/runner_test.go +46 -0
  158. package/project/.sdk/tm/go/test/struct_runner_test.go +7 -7
  159. package/project/.sdk/tm/go/test/struct_utility_test.go +63 -27
  160. package/project/.sdk/tm/go/utility/fetcher.go +2 -2
  161. package/project/.sdk/tm/go/utility/make_options.go +24 -5
  162. package/project/.sdk/tm/go/utility/make_point.go +1 -1
  163. package/project/.sdk/tm/go/utility/make_spec.go +17 -1
  164. package/project/.sdk/tm/go/utility/prepare_auth.go +1 -1
  165. package/project/.sdk/tm/go/utility/struct/voxgigstruct.go +370 -307
  166. package/project/.sdk/tm/go/utility/transform_request.go +29 -1
  167. package/project/.sdk/tm/go/utility/transform_response.go +20 -1
  168. package/project/.sdk/tm/java/test/CustomUtilityTest.java +10 -0
  169. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +7 -0
  170. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +6 -0
  171. package/project/.sdk/tm/java/test/RunnerSupport.java +43 -0
  172. package/project/.sdk/tm/java/utility/MakeOptions.java +84 -1
  173. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +24 -0
  174. package/project/.sdk/tm/js/src/utility/StructUtility.js +2076 -1837
  175. package/project/.sdk/tm/js/test/feature/Corpus.test.js +9 -0
  176. package/project/.sdk/tm/js/test/sdk-test-control.json +19 -0
  177. package/project/.sdk/tm/js/test/utility.js +91 -1
  178. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +21 -0
  179. package/project/.sdk/tm/lean/src/SdkUtility.lean +12 -1
  180. package/project/.sdk/tm/lua/test/runner.lua +40 -0
  181. package/project/.sdk/tm/ocaml/sdk_runtime.ml +11 -0
  182. package/project/.sdk/tm/ocaml/test/harness.ml +14 -4
  183. package/project/.sdk/tm/perl/t/runner.pm +30 -0
  184. package/project/.sdk/tm/perl/utility/make_options.pm +18 -0
  185. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +10 -1
  186. package/project/.sdk/tm/php/test/Runner.php +36 -0
  187. package/project/.sdk/tm/php/utility/MakeOptions.php +24 -0
  188. package/project/.sdk/tm/py/pkg/utility/make_options.py +20 -0
  189. package/project/.sdk/tm/py/test/runner.py +32 -0
  190. package/project/.sdk/tm/py/test/test_feature_corpus.py +10 -1
  191. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +9 -1
  192. package/project/.sdk/tm/rb/test/runner.rb +25 -0
  193. package/project/.sdk/tm/rb/utility/make_options.rb +17 -0
  194. package/project/.sdk/tm/rust/tests/common/mod.rs +32 -0
  195. package/project/.sdk/tm/rust/utility/make_options.rs +26 -1
  196. package/project/.sdk/tm/scala/utility/Make.scala +17 -0
  197. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -0
  198. package/project/.sdk/tm/ts/src/feature/secrets/SecretsFeature.ts +419 -0
  199. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +387 -0
  200. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +175 -0
  201. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +51 -0
  202. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/Registry.ts +90 -0
  203. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/addr.ts +48 -0
  204. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/aws.ts +219 -0
  205. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/azuresecrets.ts +144 -0
  206. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/boru.ts +113 -0
  207. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/doppler.ts +77 -0
  208. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/dotenv.ts +51 -0
  209. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/env.ts +31 -0
  210. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/file.ts +54 -0
  211. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/gcpsecrets.ts +118 -0
  212. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/hashicorp.ts +159 -0
  213. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/http.ts +66 -0
  214. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/infisical.ts +107 -0
  215. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/memory.ts +33 -0
  216. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/onepassword.ts +120 -0
  217. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/support.ts +176 -0
  218. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +18 -0
  219. package/project/.sdk/tm/ts/src/utility/StructUtility.ts +1321 -710
  220. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +9 -0
  221. package/project/.sdk/tm/ts/test/feature/secrets/Secrets.test.ts +764 -0
  222. package/project/.sdk/tm/ts/test/omni.test.ts +84 -0
  223. package/project/.sdk/tm/ts/test/omni.ts +86 -0
  224. package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +6 -2
  225. package/project/.sdk/tm/ts/test/utility/StructUtility.test.ts +2 -2
  226. package/project/.sdk/tm/ts/test/utility.ts +122 -0
  227. package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +694 -0
  228. package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +244 -0
  229. package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +188 -0
  230. package/project/.sdk/tm/ts/test/vendor/omni/index.ts +38 -0
  231. package/project/.sdk/tm/zig/core/utility.zig +24 -1
  232. package/project/sdkgen-package.json +2 -1
  233. package/src/action/check.ts +18 -0
  234. package/src/cmp/AgentGuide.ts +1 -1
  235. package/src/cmp/AgentGuideContent.ts +9 -2
  236. package/src/cmp/Feature.ts +24 -0
  237. package/src/cmp/FeatureDocs.ts +7 -1
  238. package/src/cmp/ReadmeExplanation.ts +4 -1
  239. package/src/cmp/ReadmeFeatures.ts +1 -1
  240. package/src/cmp/ReadmeRefFeatures.ts +1 -1
  241. package/src/cmp/TestControl.ts +64 -0
  242. package/src/helpers/applicability.ts +123 -0
  243. package/src/helpers/collectDeps.ts +5 -1
  244. package/src/helpers/featureSource.ts +130 -0
  245. package/src/helpers/serverVars.ts +19 -0
  246. package/src/sdkgen.ts +16 -1
  247. package/src/utility.ts +71 -1
  248. 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
+ }