@voxgig/sdkgen 4.5.3 → 4.6.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 (174) 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 +17 -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/helpers/applicability.d.ts +6 -0
  21. package/dist/helpers/applicability.js +103 -0
  22. package/dist/helpers/applicability.js.map +1 -0
  23. package/dist/helpers/collectDeps.js +4 -2
  24. package/dist/helpers/collectDeps.js.map +1 -1
  25. package/dist/helpers/serverVars.d.ts +2 -1
  26. package/dist/helpers/serverVars.js +17 -0
  27. package/dist/helpers/serverVars.js.map +1 -1
  28. package/dist/sdkgen.d.ts +3 -2
  29. package/dist/sdkgen.js +9 -2
  30. package/dist/sdkgen.js.map +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/dist/utility.js +7 -1
  33. package/dist/utility.js.map +1 -1
  34. package/model/sdkgen.aon +39 -0
  35. package/package.json +1 -1
  36. package/project/.sdk/model/feature/feature-index.aon +1 -0
  37. package/project/.sdk/model/feature/secrets.aon +118 -0
  38. package/project/.sdk/model/target/ts.aon +8 -1
  39. package/project/.sdk/src/cmp/c/Config_c.ts +5 -1
  40. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +5 -1
  41. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +5 -1
  42. package/project/.sdk/src/cmp/dart/Config_dart.ts +5 -1
  43. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +5 -1
  44. package/project/.sdk/src/cmp/elixir/Main_elixir.ts +5 -1
  45. package/project/.sdk/src/cmp/go/Config_go.ts +5 -1
  46. package/project/.sdk/src/cmp/go/Main_go.ts +6 -2
  47. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +16 -3
  48. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +16 -3
  49. package/project/.sdk/src/cmp/go/TestDirect_go.ts +28 -4
  50. package/project/.sdk/src/cmp/go/TestEntity_go.ts +43 -10
  51. package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +5 -5
  52. package/project/.sdk/src/cmp/java/Config_java.ts +22 -1
  53. package/project/.sdk/src/cmp/java/Main_java.ts +5 -1
  54. package/project/.sdk/src/cmp/java/ReadmeQuick_java.ts +17 -3
  55. package/project/.sdk/src/cmp/java/ReadmeTopQuick_java.ts +17 -3
  56. package/project/.sdk/src/cmp/java/TestDirect_java.ts +23 -4
  57. package/project/.sdk/src/cmp/java/TestEntity_java.ts +29 -4
  58. package/project/.sdk/src/cmp/js/Config_js.ts +5 -1
  59. package/project/.sdk/src/cmp/js/Main_js.ts +5 -1
  60. package/project/.sdk/src/cmp/js/Package_js.ts +4 -1
  61. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +5 -1
  62. package/project/.sdk/src/cmp/lean/Config_lean.ts +5 -1
  63. package/project/.sdk/src/cmp/lua/Config_lua.ts +5 -1
  64. package/project/.sdk/src/cmp/lua/Main_lua.ts +5 -1
  65. package/project/.sdk/src/cmp/lua/Package_lua.ts +4 -1
  66. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +5 -1
  67. package/project/.sdk/src/cmp/perl/Main_perl.ts +5 -1
  68. package/project/.sdk/src/cmp/php/Config_php.ts +5 -1
  69. package/project/.sdk/src/cmp/php/Main_php.ts +5 -1
  70. package/project/.sdk/src/cmp/py/Config_py.ts +5 -1
  71. package/project/.sdk/src/cmp/py/Main_py.ts +5 -1
  72. package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +16 -3
  73. package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +16 -3
  74. package/project/.sdk/src/cmp/py/TestDirect_py.ts +24 -5
  75. package/project/.sdk/src/cmp/py/TestEntity_py.ts +23 -3
  76. package/project/.sdk/src/cmp/rb/Config_rb.ts +5 -1
  77. package/project/.sdk/src/cmp/rb/Main_rb.ts +5 -1
  78. package/project/.sdk/src/cmp/rust/Config_rust.ts +5 -1
  79. package/project/.sdk/src/cmp/rust/Main_rust.ts +5 -1
  80. package/project/.sdk/src/cmp/scala/Config_scala.ts +5 -1
  81. package/project/.sdk/src/cmp/scala/Main_scala.ts +5 -1
  82. package/project/.sdk/src/cmp/swift/Config_swift.ts +5 -1
  83. package/project/.sdk/src/cmp/ts/Config_ts.ts +5 -1
  84. package/project/.sdk/src/cmp/ts/Main_ts.ts +57 -1
  85. package/project/.sdk/src/cmp/ts/Package_ts.ts +4 -1
  86. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +15 -3
  87. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +18 -5
  88. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +19 -5
  89. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +67 -7
  90. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +27 -4
  91. package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +9 -2
  92. package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +9 -2
  93. package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +7 -0
  94. package/project/.sdk/src/cmp/zig/Config_zig.ts +5 -1
  95. package/project/.sdk/tm/c/utility/make_options.c +22 -0
  96. package/project/.sdk/tm/clojure/src/sdk/core.clj +18 -1
  97. package/project/.sdk/tm/cpp/utility/pipeline.hpp +23 -0
  98. package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +15 -0
  99. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +23 -0
  100. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +22 -0
  101. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +19 -0
  102. package/project/.sdk/tm/go/core/context.go +1 -1
  103. package/project/.sdk/tm/go/feature/cost_feature.go +1 -1
  104. package/project/.sdk/tm/go/feature/paging_feature.go +5 -5
  105. package/project/.sdk/tm/go/feature/test_feature.go +12 -5
  106. package/project/.sdk/tm/go/test/custom_utility_test.go +15 -0
  107. package/project/.sdk/tm/go/test/feature_corpus_test.go +7 -0
  108. package/project/.sdk/tm/go/test/runner_test.go +46 -0
  109. package/project/.sdk/tm/go/test/struct_runner_test.go +7 -7
  110. package/project/.sdk/tm/go/test/struct_utility_test.go +63 -27
  111. package/project/.sdk/tm/go/utility/fetcher.go +2 -2
  112. package/project/.sdk/tm/go/utility/make_options.go +24 -5
  113. package/project/.sdk/tm/go/utility/make_point.go +1 -1
  114. package/project/.sdk/tm/go/utility/make_spec.go +17 -1
  115. package/project/.sdk/tm/go/utility/prepare_auth.go +1 -1
  116. package/project/.sdk/tm/go/utility/struct/voxgigstruct.go +370 -307
  117. package/project/.sdk/tm/go/utility/transform_request.go +29 -1
  118. package/project/.sdk/tm/go/utility/transform_response.go +20 -1
  119. package/project/.sdk/tm/java/test/CustomUtilityTest.java +10 -0
  120. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +7 -0
  121. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +6 -0
  122. package/project/.sdk/tm/java/test/RunnerSupport.java +43 -0
  123. package/project/.sdk/tm/java/utility/MakeOptions.java +84 -1
  124. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +24 -0
  125. package/project/.sdk/tm/js/src/utility/StructUtility.js +2076 -1837
  126. package/project/.sdk/tm/js/test/feature/Corpus.test.js +9 -0
  127. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +21 -0
  128. package/project/.sdk/tm/lean/src/SdkUtility.lean +12 -1
  129. package/project/.sdk/tm/ocaml/sdk_runtime.ml +11 -0
  130. package/project/.sdk/tm/perl/utility/make_options.pm +18 -0
  131. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +10 -1
  132. package/project/.sdk/tm/php/utility/MakeOptions.php +24 -0
  133. package/project/.sdk/tm/py/pkg/utility/make_options.py +20 -0
  134. package/project/.sdk/tm/py/test/runner.py +32 -0
  135. package/project/.sdk/tm/py/test/test_feature_corpus.py +10 -1
  136. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +9 -1
  137. package/project/.sdk/tm/rb/utility/make_options.rb +17 -0
  138. package/project/.sdk/tm/rust/utility/make_options.rs +25 -0
  139. package/project/.sdk/tm/scala/utility/Make.scala +17 -0
  140. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -0
  141. package/project/.sdk/tm/ts/src/feature/secrets/SecretsFeature.ts +419 -0
  142. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Providers.ts +1221 -0
  143. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +381 -0
  144. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +151 -0
  145. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +42 -0
  146. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +18 -0
  147. package/project/.sdk/tm/ts/src/utility/StructUtility.ts +1321 -710
  148. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +9 -0
  149. package/project/.sdk/tm/ts/test/feature/secrets/Secrets.test.ts +764 -0
  150. package/project/.sdk/tm/ts/test/omni.test.ts +84 -0
  151. package/project/.sdk/tm/ts/test/omni.ts +86 -0
  152. package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +6 -2
  153. package/project/.sdk/tm/ts/test/utility/StructUtility.test.ts +2 -2
  154. package/project/.sdk/tm/ts/test/utility.ts +122 -0
  155. package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +692 -0
  156. package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +238 -0
  157. package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +188 -0
  158. package/project/.sdk/tm/ts/test/vendor/omni/index.ts +38 -0
  159. package/project/.sdk/tm/zig/core/utility.zig +23 -0
  160. package/project/sdkgen-package.json +2 -1
  161. package/src/action/check.ts +18 -0
  162. package/src/cmp/AgentGuide.ts +1 -1
  163. package/src/cmp/AgentGuideContent.ts +9 -2
  164. package/src/cmp/Feature.ts +19 -0
  165. package/src/cmp/FeatureDocs.ts +7 -1
  166. package/src/cmp/ReadmeExplanation.ts +4 -1
  167. package/src/cmp/ReadmeFeatures.ts +1 -1
  168. package/src/cmp/ReadmeRefFeatures.ts +1 -1
  169. package/src/helpers/applicability.ts +123 -0
  170. package/src/helpers/collectDeps.ts +5 -1
  171. package/src/helpers/serverVars.ts +19 -0
  172. package/src/sdkgen.ts +9 -1
  173. package/src/utility.ts +10 -1
  174. package/project/.sdk/tm/ts/test/runner.ts +0 -425
@@ -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
+ }