@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
@@ -17,9 +17,12 @@ import {
17
17
  cmp,
18
18
  snakify,
19
19
  isAuthActive,
20
+ serverVarEnv,
21
+ serverVariables,
20
22
  isHttpBasicAuth,
21
23
  jsProp,
22
- jsOptProp, envName, envToken, liveStrict
24
+ jsOptProp, envName, envToken, liveStrict,
25
+ jsKey
23
26
  } from '@voxgig/sdkgen'
24
27
 
25
28
 
@@ -47,7 +50,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
47
50
  const authActive = isAuthActive(model)
48
51
  const authBasic = authActive && isHttpBasicAuth(model)
49
52
  const apikeyEnvEntry = authActive
50
- ? `\n '${PROJECTNAME}_APIKEY': 'NONE',${authBasic ? `\n '${PROJECTNAME}_SECRET': 'NONE',` : ''}`
53
+ ? `\n '${PROJECTNAME}_APIKEY': '',${authBasic ? `\n '${PROJECTNAME}_SECRET': '',` : ''}`
51
54
  : ''
52
55
  const apikeyLiveField = authActive
53
56
  ? `
@@ -55,6 +58,25 @@ const TestDirect = cmp(function TestDirect(props: any) {
55
58
  secret: env.${PROJECTNAME}_SECRET,` : ''}`
56
59
  : ''
57
60
 
61
+ // A templated server URL (OpenAPI server variables) makes a LIVE client
62
+ // impossible to construct without values: makeOptions raises rather than
63
+ // request a URL with a literal `{account_id}` in it. Taken from the
64
+ // environment, the same way the apikey is.
65
+ //
66
+ // Keys are quoted and the env read is bracketed via jsKey/jsProp: a server
67
+ // variable name is spec-derived and need not be a JS identifier — the URL
68
+ // grammar admits a leading digit ({2fa}), and a declared-but-unreferenced
69
+ // variable ({edge-zone}) is not constrained at all. Bare `name:` and
70
+ // `env.PROJ_SERVER_EDGE-ZONE` are both syntax errors.
71
+ const svars = serverVariables(model)
72
+ const serverEnvEntry = svars
73
+ .map((v: any) => `\n '${serverVarEnv(PROJECTNAME, v.name)}': ${JSON.stringify(v.dflt)},`).join('')
74
+ const serverLiveField = 0 === svars.length ? '' : `
75
+ server: {${svars
76
+ .map((v: any) => `
77
+ ${jsKey(v.name)}: ${jsProp('env', serverVarEnv(PROJECTNAME, v.name))},`).join('')}
78
+ },`
79
+
58
80
  const opnames = Object.keys(entity.op || {})
59
81
  const hasLoad = opnames.includes('load')
60
82
  const hasList = opnames.includes('list')
@@ -86,14 +108,17 @@ function directSetup(mockres?: any) {
86
108
 
87
109
  const env = envOverride({
88
110
  '${entidEnvVar}': {},
89
- '${PROJECTNAME}_TEST_LIVE': 'FALSE',${apikeyEnvEntry}
111
+ '${PROJECTNAME}_TEST_LIVE': 'FALSE',${apikeyEnvEntry}${serverEnvEntry}
90
112
  })
91
113
 
92
114
  const live = 'TRUE' === env.${PROJECTNAME}_TEST_LIVE
93
115
 
94
116
  if (live) {
95
- const client = new ${nom(model.const, 'Name')}SDK({${apikeyLiveField}
96
- })
117
+ // Merged so the generated fields win: sdk-test-control.json's
118
+ // test.client.options adds to the live client, it does not redirect it.
119
+ const client = new ${nom(model.const, 'Name')}SDK(
120
+ Object.assign({}, liveClientOptions(), {${apikeyLiveField}${serverLiveField}
121
+ }))
97
122
 
98
123
  let idmap: any = env['${entidEnvVar}']
99
124
  if ('string' === typeof idmap && idmap.startsWith('{')) {
@@ -210,7 +235,21 @@ function generateDirectGraphql(
210
235
  ${varAsserts}`
211
236
 
212
237
  const checks = strict ?
213
- offlineChecks.replace(/^ {6}/gm, ' ').replace(/^ {4}$/gm, '') :
238
+ ` if (setup.live) {
239
+ // STRICT live mode: a non-2xx is a real failure - this project owns
240
+ // the server it points at, so there is nothing to be lenient about.
241
+ //
242
+ // What is NOT asserted here is the MOCK's own fixtures. \`direct01\`
243
+ // is a scripted id and \`calls\` records the mock transport; neither
244
+ // exists on a live run, so asserting them made strict mode mean
245
+ // "compare the live server against the mock's script" - a suite that
246
+ // could not pass against any real API, including this project's own.
247
+ assert(result.ok === true,
248
+ 'live request failed: ' + result.status + ' ' + JSON.stringify(result.data))
249
+ assert(result.status >= 200 && result.status < 300)
250
+ assert(null != result.data)
251
+ } else {
252
+ ${offlineChecks} }` :
214
253
  ` if (setup.live) {
215
254
  // Live mode is lenient: synthetic ids frequently fail server-side
216
255
  // validation. Skip rather than fail when the call doesn't come back
@@ -429,7 +468,21 @@ ${loadParams.map((p: any, i: number) => ` ${jsProp('params', p.name)} = 'di
429
468
  ${paramAsserts}`
430
469
 
431
470
  const loadChecks = strict ?
432
- offlineChecks.replace(/^ {6}/gm, ' ').replace(/^ {4}$/gm, '') :
471
+ ` if (setup.live) {
472
+ // STRICT live mode: a non-2xx is a real failure - this project owns
473
+ // the server it points at, so there is nothing to be lenient about.
474
+ //
475
+ // What is NOT asserted here is the MOCK's own fixtures. \`direct01\`
476
+ // is a scripted id and \`calls\` records the mock transport; neither
477
+ // exists on a live run, so asserting them made strict mode mean
478
+ // "compare the live server against the mock's script" - a suite that
479
+ // could not pass against any real API, including this project's own.
480
+ assert(result.ok === true,
481
+ 'live request failed: ' + result.status + ' ' + JSON.stringify(result.data))
482
+ assert(result.status >= 200 && result.status < 300)
483
+ assert(null != result.data)
484
+ } else {
485
+ ${offlineChecks} }` :
433
486
  ` if (setup.live) {
434
487
  // Live mode is lenient: synthetic IDs frequently 4xx. Skip rather
435
488
  // than fail when the load endpoint isn't reachable with the IDs we
@@ -543,7 +596,21 @@ ${mockLines}
543
596
  ${paramAsserts}`
544
597
 
545
598
  const listChecks = strict ?
546
- offlineChecks.replace(/^ {6}/gm, ' ').replace(/^ {4}$/gm, '') :
599
+ ` if (setup.live) {
600
+ // STRICT live mode: a non-2xx is a real failure - this project owns
601
+ // the server it points at, so there is nothing to be lenient about.
602
+ //
603
+ // What is NOT asserted here is the MOCK's own fixtures. \`direct01\`
604
+ // is a scripted id and \`calls\` records the mock transport; neither
605
+ // exists on a live run, so asserting them made strict mode mean
606
+ // "compare the live server against the mock's script" - a suite that
607
+ // could not pass against any real API, including this project's own.
608
+ assert(result.ok === true,
609
+ 'live request failed: ' + result.status + ' ' + JSON.stringify(result.data))
610
+ assert(result.status >= 200 && result.status < 300)
611
+ assert(null != result.data)
612
+ } else {
613
+ ${offlineChecks} }` :
547
614
  ` if (setup.live) {
548
615
  // Live mode is lenient: synthetic IDs frequently 4xx and the list-
549
616
  // response shape varies wildly across public APIs. Skip rather than
@@ -30,8 +30,12 @@ import {
30
30
  cmp,
31
31
  each,
32
32
  isAuthActive,
33
+ serverVarEnv,
34
+ serverVariables,
33
35
  isHttpBasicAuth,
34
- entityDataIdField, envName, envToken
36
+ entityDataIdField, envName, envToken,
37
+ jsKey,
38
+ jsProp
35
39
  } from '@voxgig/sdkgen'
36
40
 
37
41
 
@@ -67,7 +71,7 @@ const TestEntity = cmp(function TestEntity(props: any) {
67
71
  const authActive = isAuthActive(model)
68
72
  const authBasic = authActive && isHttpBasicAuth(model)
69
73
  const apikeyEnvEntry = authActive
70
- ? `\n '${PROJENVNAME}_APIKEY': 'NONE',${authBasic ? `\n '${PROJENVNAME}_SECRET': 'NONE',` : ''}`
74
+ ? `\n '${PROJENVNAME}_APIKEY': '',${authBasic ? `\n '${PROJENVNAME}_SECRET': '',` : ''}`
71
75
  : ''
72
76
  const apikeyLiveField = authActive
73
77
  ? `
@@ -75,6 +79,25 @@ const TestEntity = cmp(function TestEntity(props: any) {
75
79
  secret: env.${PROJENVNAME}_SECRET,` : ''}`
76
80
  : ''
77
81
 
82
+ // A templated server URL (OpenAPI server variables) makes a LIVE client
83
+ // impossible to construct without values: makeOptions raises rather than
84
+ // request a URL with a literal `{account_id}` in it. So the live suite
85
+ // takes them from the environment the same way it takes the apikey.
86
+ //
87
+ // Keys are quoted and the env read is bracketed via jsKey/jsProp: a server
88
+ // variable name is spec-derived and need not be a JS identifier — the URL
89
+ // grammar admits a leading digit ({2fa}), and a declared-but-unreferenced
90
+ // variable ({edge-zone}) is not constrained at all. Bare `name:` and
91
+ // `env.PROJ_SERVER_EDGE-ZONE` are both syntax errors.
92
+ const svars = serverVariables(model)
93
+ const serverEnvEntry = svars
94
+ .map((v: any) => `\n '${serverVarEnv(PROJENVNAME, v.name)}': ${JSON.stringify(v.dflt)},`).join('')
95
+ const serverLiveField = 0 === svars.length ? '' : `
96
+ server: {${svars
97
+ .map((v: any) => `
98
+ ${jsKey(v.name)}: ${jsProp('env', serverVarEnv(PROJENVNAME, v.name))},`).join('')}
99
+ },`
100
+
78
101
  // TODO: should be a utility function
79
102
  const ff = projectPath('src/cmp/ts/fragment/')
80
103
 
@@ -156,7 +179,7 @@ function basicSetup(extra?: any) {
156
179
  const env = envOverride({
157
180
  '${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID': idmap,
158
181
  '${PROJENVNAME}_TEST_LIVE': 'FALSE',
159
- '${PROJENVNAME}_TEST_EXPLAIN': 'FALSE',${apikeyEnvEntry}
182
+ '${PROJENVNAME}_TEST_EXPLAIN': 'FALSE',${apikeyEnvEntry}${serverEnvEntry}
160
183
  })
161
184
 
162
185
  idmap = env['${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID']
@@ -165,9 +188,17 @@ function basicSetup(extra?: any) {
165
188
 
166
189
  if (live) {
167
190
  client = new ${model.Name}SDK(merge([
168
- {${apikeyLiveField}
191
+ // FIRST, so the generated fields below win: sdk-test-control.json's
192
+ // test.client.options adds to the live client, it does not redirect it.
193
+ liveClientOptions(),
194
+ {${apikeyLiveField}${serverLiveField}
169
195
  },
170
- extra
196
+ // 'extra || {}', not a bare 'extra': struct.merge returns UNDEFINED when the
197
+ // last entry is undefined, and basicSetup is normally called with no
198
+ // argument at all - so a bare 'extra' silently discarded the apikey
199
+ // and server values above and handed the SDK undefined. Harmless
200
+ // while there was nothing in that object; not harmless now.
201
+ extra || {}
171
202
  ]))
172
203
  }
173
204
 
@@ -3,7 +3,8 @@ import type {
3
3
  ModelEntity
4
4
  } from '@voxgig/apidef'
5
5
 
6
- import { cmp, each, Folder, entityCollection } from '@voxgig/sdkgen'
6
+ import { cmp, each, Folder, entityCollection,
7
+ TestControl } from '@voxgig/sdkgen'
7
8
 
8
9
 
9
10
  // import { Quick } from './Quick_ts'
@@ -19,6 +20,9 @@ const Test = cmp(function Test(props: any) {
19
20
  const { target } = props
20
21
 
21
22
  Folder({ name: 'test' }, () => {
23
+
24
+ // Write-once: a project's edited control file survives regeneration.
25
+ TestControl({ target, dir: 'test' })
22
26
  // Quick({ target })
23
27
  // TestMain({ target })
24
28
 
@@ -1,6 +1,4 @@
1
1
 
2
- const envlocal = __dirname + '/../../../.env.local'
3
- require('dotenv').config({ quiet: true, path: [envlocal] })
4
2
 
5
3
  import { test, describe, afterEach } from 'node:test'
6
4
  import assert from 'node:assert'
@@ -10,12 +8,21 @@ import { ProjectNameSDK } from '../../..'
10
8
 
11
9
  import {
12
10
  envOverride,
11
+ liveClientOptions,
13
12
  liveDelay,
13
+ loadEnvLocal,
14
14
  maybeSkipControl,
15
15
  skipIfMissingIds,
16
16
  } from '../../utility'
17
17
 
18
18
 
19
+ // AFTER the imports on purpose: TypeScript hoists `import` above any
20
+ // statement in the emitted CommonJS, so a loader placed above them would
21
+ // run only after every imported module had already been evaluated - and
22
+ // anything reading process.env at module scope would miss these values.
23
+ loadEnvLocal(__dirname + '/../../../.env.local')
24
+
25
+
19
26
  describe('EntityNameDirect', async () => {
20
27
 
21
28
  // Per-test live pacing. Delay is read from sdk-test-control.json's
@@ -1,6 +1,4 @@
1
1
 
2
- const envlocal = __dirname + '/../../../.env.local'
3
- require('dotenv').config({ quiet: true, path: [envlocal] })
4
2
 
5
3
  import Path from 'node:path'
6
4
  import * as Fs from 'node:fs'
@@ -13,7 +11,9 @@ import { ProjectNameSDK, BaseFeature, stdutil } from '../../..'
13
11
 
14
12
  import {
15
13
  envOverride,
14
+ liveClientOptions,
16
15
  liveDelay,
16
+ loadEnvLocal,
17
17
  makeCtrl,
18
18
  makeMatch,
19
19
  makeReqdata,
@@ -23,6 +23,13 @@ import {
23
23
  } from '../../utility'
24
24
 
25
25
 
26
+ // AFTER the imports on purpose: TypeScript hoists `import` above any
27
+ // statement in the emitted CommonJS, so a loader placed above them would
28
+ // run only after every imported module had already been evaluated - and
29
+ // anything reading process.env at module scope would miss these values.
30
+ loadEnvLocal(__dirname + '/../../../.env.local')
31
+
32
+
26
33
  describe('EntityNameEntity', async () => {
27
34
 
28
35
  // Per-test live pacing. Delay is read from sdk-test-control.json's
@@ -9,6 +9,7 @@ import { Utility } from './utility/Utility'
9
9
 
10
10
 
11
11
  import { BaseFeature } from './feature/base/BaseFeature'
12
+ // #SecretsImport
12
13
 
13
14
 
14
15
  const stdutil = new Utility()
@@ -20,6 +21,7 @@ class ProjectNameSDK {
20
21
  _utility = new Utility()
21
22
  _features: Feature[]
22
23
  _rootctx: Context
24
+ // #SecretsField
23
25
 
24
26
  constructor(options?: any) {
25
27
 
@@ -92,6 +94,8 @@ class ProjectNameSDK {
92
94
  return this._utility.struct.clone(this._utility)
93
95
  }
94
96
 
97
+ // #SecretsAccessor
98
+
95
99
 
96
100
  async prepare(fetchargs?: any) {
97
101
  const utility = this._utility
@@ -138,6 +142,8 @@ class ProjectNameSDK {
138
142
  }
139
143
  }
140
144
 
145
+ // #SecretsResolve
146
+
141
147
  // Apply SDK auth (apikey, auth prefix, etc.)
142
148
  const authResult = prepareAuth(ctx)
143
149
  if (authResult instanceof Error) {
@@ -334,6 +340,7 @@ const SDK = ProjectNameSDK
334
340
  export {
335
341
  stdutil,
336
342
  config,
343
+ // #SecretsExport
337
344
 
338
345
  BaseFeature,
339
346
  ProjectNameEntityBase,
@@ -8,6 +8,7 @@ import {
8
8
  each,
9
9
  isAuthActive,
10
10
  isConfigData,
11
+ targetFeatures,
11
12
  } from '@voxgig/sdkgen'
12
13
 
13
14
 
@@ -32,7 +33,10 @@ const Config = cmp(async function Config(props: any) {
32
33
 
33
34
  const model: Model = ctx$.model
34
35
 
35
- const feature = getModelPath(model, `main.${KIT}.feature`)
36
+ // Gated by the applicability tags, so this target never imports or
37
+ // registers a feature it has no source for. One rule, one place:
38
+ // helpers/applicability.
39
+ const feature = targetFeatures(model, target)
36
40
 
37
41
  // The canonical config OBJECT and its JSON, from the shared helper. Both
38
42
  // representations render from the same `def`, so they cannot describe
@@ -22,6 +22,23 @@ voxgig_value* make_options_util(Context* ctx) {
22
22
  }
23
23
  }
24
24
 
25
+ /* `auth: null` is the documented way to disable auth outright, and
26
+ * prepare_auth honours it before it ever reads the apikey. validate would
27
+ * erase it: this port follows the Group A rule, so a stored null reads as
28
+ * "no value" and the optspec's `auth` default fires instead - transmitting
29
+ * the credential the caller withheld. Put the null back afterwards.
30
+ *
31
+ * Unlike js, no delete-before-validate is needed here: Group A means
32
+ * validate DEFAULTS rather than rejecting, so the key can travel through.
33
+ *
34
+ * Read the map DIRECTLY rather than through getp, which applies that same
35
+ * rule and so cannot tell an absent auth from a suppressed one. */
36
+ bool auth_suppressed = false;
37
+ if (voxgig_is_map(options)) {
38
+ voxgig_value* authval = voxgig_map_get(voxgig_as_map(options), "auth");
39
+ auth_suppressed = (NULL != authval && voxgig_is_null(authval));
40
+ }
41
+
25
42
  voxgig_value* opts = voxgig_clone(options);
26
43
 
27
44
  // Feature add-order. options.feature may be an ordered list of
@@ -93,6 +110,11 @@ voxgig_value* make_options_util(Context* ctx) {
93
110
  opts = validated;
94
111
  }
95
112
 
113
+ /* Restore the suppression the optspec default would otherwise erase. */
114
+ if (auth_suppressed) {
115
+ setp(opts, "auth", voxgig_new_null());
116
+ }
117
+
96
118
  // Restore system.fetch.
97
119
  if (!v_is_noval(sys_fetch)) {
98
120
  voxgig_value* sys = getp(opts, "system");
@@ -988,7 +988,22 @@
988
988
  (if (and member (not= member :custom) (contains? members member))
989
989
  (oset! util member v)
990
990
  (.put ^java.util.Map (oget util :custom) k v))))))
991
- (let [opts0 (let [c (vs/clone options)] (if (vs/ismap c) c (vs/jm)))
991
+ (let [;; `auth` nil is the documented way to disable auth outright, and
992
+ ;; u-prepare-auth honours it before it ever reads the apikey. It
993
+ ;; cannot survive validate: a stored null reads as "no value", so the
994
+ ;; optspec's `auth` default fires and the suppression silently
995
+ ;; becomes "use default auth" - transmitting the credential the
996
+ ;; caller withheld. Withhold the key for validate, then put the nil
997
+ ;; back. Same fix as ts/js/go makeOptions.
998
+ ;;
999
+ ;; `.containsKey` rather than a nil check on the value: the latter
1000
+ ;; cannot tell an ABSENT auth from a suppressed one, and only the
1001
+ ;; second is a suppression.
1002
+ auth-suppressed (and (vs/ismap options)
1003
+ (.containsKey ^java.util.Map options "auth")
1004
+ (nil? (.get ^java.util.Map options "auth")))
1005
+ opts0 (let [c (vs/clone options)] (if (vs/ismap c) c (vs/jm)))
1006
+ _ (when auth-suppressed (.remove ^java.util.Map opts0 "auth"))
992
1007
  ;; Feature add-order. options.feature may be given as an ordered
993
1008
  ;; ARRAY of {name active ...opts} entries (array position = add
994
1009
  ;; order) or a {name {opts}} map. Normalize an array to a map (so
@@ -1037,6 +1052,8 @@
1037
1052
  merged (vs/merge (vs/jt (vs/jm) cfgopts opts0))
1038
1053
  validated (vs/validate merged optspec)
1039
1054
  opts (if (vs/ismap validated) validated (vs/jm))
1055
+ ;; Restore the suppression the optspec default would otherwise erase.
1056
+ _ (when auth-suppressed (.put ^java.util.Map opts "auth" nil))
1040
1057
  ;; Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
1041
1058
  ;; Every placeholder must resolve to a non-empty value: from
1042
1059
  ;; options["server"] (user), else the config default. A placeholder
@@ -1107,8 +1107,26 @@ inline Value makeOptions(CtxPtr ctx) {
1107
1107
  }
1108
1108
  }
1109
1109
 
1110
+ // `auth: null` is the documented way to disable auth outright, and
1111
+ // prepareAuth honours it before it ever reads the apikey. It cannot survive
1112
+ // validate: this port follows the Group A rule, so a stored null reads as
1113
+ // "no value" and the optspec's `auth` default fires instead - transmitting
1114
+ // the credential the caller withheld. Withhold the key for validate, then
1115
+ // put the null back. Same fix as ts/js/go makeOptions.
1116
+ //
1117
+ // mapget rather than getp: getp applies Group A and collapses a stored null
1118
+ // to undef, so it cannot tell an ABSENT auth from a suppressed one. And the
1119
+ // test is is_null, NOT is_nullish - a present-but-UNDEF auth is a missing
1120
+ // value, which the ts/js reference lets the optspec default fill in. Only an
1121
+ // explicit null is a suppression.
1122
+ bool authSuppressed = mapget(options, "auth").is_null();
1123
+
1110
1124
  Value opts = Struct::clone(options);
1111
1125
 
1126
+ if (authSuppressed) {
1127
+ map_remove(opts, "auth");
1128
+ }
1129
+
1112
1130
  // Feature add-order. options.feature may be an ordered list of
1113
1131
  // { name, active, ...opts } entries (the list position IS the order in which
1114
1132
  // features are added), or a { name: {opts} } map. Normalize a list to a map
@@ -1159,6 +1177,11 @@ inline Value makeOptions(CtxPtr ctx) {
1159
1177
  Value validated = Struct::validate(merged, optspec, vopts);
1160
1178
  opts = validated;
1161
1179
 
1180
+ // Restore the suppression the optspec default would otherwise erase.
1181
+ if (authSuppressed) {
1182
+ map_put(opts, "auth", Value(nullptr));
1183
+ }
1184
+
1162
1185
  if (!is_nullish(sysFetch)) {
1163
1186
  Value sys = Helpers::toMapAny(getp(opts, "system"));
1164
1187
  if (sys.is_map()) {
@@ -85,8 +85,13 @@ public class CustomUtilityTest
85
85
  // transport: 'base' - it REPLACES the transport by design - so a client
86
86
  // in test mode would shadow the scripted fetcher and this would assert
87
87
  // nothing.
88
+ // ["test"] is the OPTION, not the `test` FEATURE: it says "this
89
+ // client is not live", so a REQUIRED OpenAPI server variable resolves
90
+ // to a deterministic test-<name> instead of failing construction. It
91
+ // installs no transport, so the override under test still stands.
88
92
  var client = new ProjectNameSDK(new Dictionary<string, object?>
89
93
  {
94
+ ["test"] = new Dictionary<string, object?> { ["active"] = true },
90
95
  ["utility"] = new Dictionary<string, object?> { ["fetcher"] = scripted },
91
96
  });
92
97
 
@@ -122,8 +127,13 @@ public class CustomUtilityTest
122
127
  };
123
128
  };
124
129
 
130
+ // ["test"] is the OPTION, not the `test` FEATURE: it says "this
131
+ // client is not live", so a REQUIRED OpenAPI server variable resolves
132
+ // to a deterministic test-<name> instead of failing construction. It
133
+ // installs no transport, so the override under test still stands.
125
134
  var client = new ProjectNameSDK(new Dictionary<string, object?>
126
135
  {
136
+ ["test"] = new Dictionary<string, object?> { ["active"] = true },
127
137
  ["utility"] = new Dictionary<string, object?> { ["fetcher"] = scripted },
128
138
  });
129
139
 
@@ -141,8 +151,13 @@ public class CustomUtilityTest
141
151
  [Fact]
142
152
  public void AnUnknownKeyIsStillAttached()
143
153
  {
154
+ // ["test"] is the OPTION, not the `test` FEATURE: it says "this
155
+ // client is not live", so a REQUIRED OpenAPI server variable resolves
156
+ // to a deterministic test-<name> instead of failing construction. It
157
+ // installs no transport, so the override under test still stands.
144
158
  var client = new ProjectNameSDK(new Dictionary<string, object?>
145
159
  {
160
+ ["test"] = new Dictionary<string, object?> { ["active"] = true },
146
161
  ["utility"] = new Dictionary<string, object?>
147
162
  {
148
163
  ["notAUtilityMember"] = Util("EXTRA"),
@@ -153,6 +153,51 @@ public static class TestRunner
153
153
  return _testControl;
154
154
  }
155
155
 
156
+ // LiveClientOptions returns the extra SDK options every LIVE client is
157
+ // constructed with, read from sdk-test-control.json `test.client.options`.
158
+ //
159
+ // The generated live client knows two things: the base URL (from the
160
+ // spec) and the credential (from the environment). Everything else about
161
+ // how a particular API wants to be talked to - which features to switch
162
+ // on, and with what settings - is a property of THAT API, known to the
163
+ // project and to nothing in the toolchain.
164
+ //
165
+ // Merged UNDER the generated fields, so the suite's own
166
+ // base/apikey/server values win: this ADDS to the live client, it does
167
+ // not redirect it.
168
+ //
169
+ // Reserved fields are stripped HERE rather than at each merge site: the
170
+ // generated dictionary only names a field when the model calls for one,
171
+ // so a "base" in this block would face no competing value and would
172
+ // silently redirect the whole suite - credential included - to another
173
+ // host.
174
+ private static readonly string[] LiveReserved =
175
+ { "base", "prefix", "suffix", "server", "apikey", "secret" };
176
+
177
+ public static Dictionary<string, object?> LiveClientOptions()
178
+ {
179
+ var ctrl = LoadTestControl();
180
+ if (!ctrl.TryGetValue("test", out var testRaw) ||
181
+ testRaw is not Dictionary<string, object?> test ||
182
+ !test.TryGetValue("client", out var clientRaw) ||
183
+ clientRaw is not Dictionary<string, object?> client ||
184
+ !client.TryGetValue("options", out var optsRaw) ||
185
+ optsRaw is not Dictionary<string, object?> opts)
186
+ {
187
+ return new Dictionary<string, object?>();
188
+ }
189
+
190
+ var out_ = new Dictionary<string, object?>();
191
+ foreach (var kv in opts)
192
+ {
193
+ if (Array.IndexOf(LiveReserved, kv.Key) < 0)
194
+ {
195
+ out_[kv.Key] = kv.Value;
196
+ }
197
+ }
198
+ return out_;
199
+ }
200
+
156
201
  // IsControlSkipped checks sdk-test-control.json for a skip entry.
157
202
  // Returns (skip, reason).
158
203
  public static (bool, string) IsControlSkipped(string kind, string name, string mode)
@@ -114,9 +114,26 @@ public static partial class SdkUtility
114
114
  }
115
115
  }
116
116
 
117
+ // `auth: null` is the documented way to disable auth outright, and
118
+ // PrepareAuth honours it before it ever reads the apikey. It cannot
119
+ // survive validate: depending on the struct port a stored null is
120
+ // either REPLACED by the optspec default - transmitting the credential
121
+ // the caller withheld - or REJECTED outright. Withhold the key for
122
+ // validate, then put the null back. Same fix as ts/js/go makeOptions.
123
+ //
124
+ // Suppliedness cannot be recovered after validate, hence here, and it
125
+ // must tell an ABSENT auth from a present null: TryGetValue rather
126
+ // than an indexer null check, which cannot distinguish them.
127
+ var authSuppressed = options.TryGetValue("auth", out var authval) && null == authval;
128
+
117
129
  var opts = StructUtils.Clone(options) as Dictionary<string, object?>
118
130
  ?? new Dictionary<string, object?>();
119
131
 
132
+ if (authSuppressed)
133
+ {
134
+ opts.Remove("auth");
135
+ }
136
+
120
137
  // Feature add-order. options.feature may be given as an ordered LIST of
121
138
  // { name, active, ...opts } entries (the list position IS the order in
122
139
  // which features are added), or as a { name: {opts} } map. Normalize a
@@ -230,6 +247,12 @@ public static partial class SdkUtility
230
247
  var validated = StructUtils.Validate(merged, optspec);
231
248
  opts = validated as Dictionary<string, object?> ?? new Dictionary<string, object?>();
232
249
 
250
+ // Restore the suppression the optspec default would otherwise erase.
251
+ if (authSuppressed)
252
+ {
253
+ opts["auth"] = null;
254
+ }
255
+
233
256
  // Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
234
257
  // Every placeholder must resolve to a non-empty value: from
235
258
  // options.server (user), else the Config default. A placeholder that
@@ -106,12 +106,34 @@ dynamic makeOptions(dynamic ctx) {
106
106
  mergeOptions['feature'] = fmap;
107
107
  }
108
108
 
109
+ // `auth: null` is the documented way to disable auth outright, and
110
+ // prepareAuth honours it before it ever reads the apikey. It cannot survive
111
+ // validate: depending on the struct port a stored null is either REPLACED
112
+ // by the optspec default — transmitting the credential the caller withheld
113
+ // — or REJECTED outright. Withhold the key for validate, then put the null
114
+ // back. Same fix as ts/js/go makeOptions.
115
+ //
116
+ // Suppliedness cannot be recovered after validate, hence here, and it must
117
+ // tell an ABSENT auth from a present null: containsKey rather than a null
118
+ // check on the value, which cannot distinguish them.
119
+ final authSuppressed =
120
+ options is Map && options.containsKey('auth') && null == options['auth'];
121
+
109
122
  // User option maps are cloned first — their (possibly narrow) literal
110
123
  // types must not constrain the merged structures.
111
124
  dynamic opts = vs.merge([{}, cfgopts, vs.clone(mergeOptions)]);
112
125
 
126
+ if (authSuppressed && opts is Map) {
127
+ opts.remove('auth');
128
+ }
129
+
113
130
  opts = vs.validate(opts, optspec);
114
131
 
132
+ // Restore the suppression the optspec default would otherwise erase.
133
+ if (authSuppressed && opts is Map) {
134
+ opts['auth'] = null;
135
+ }
136
+
115
137
  // Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
116
138
  // Every placeholder must resolve to a non-empty value: from options.server
117
139
  // (user), else the Config default. A placeholder that resolves to '' is a