@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.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/action/check.js +15 -0
- package/dist/action/check.js.map +1 -1
- package/dist/cmp/AgentGuide.js +1 -1
- package/dist/cmp/AgentGuide.js.map +1 -1
- package/dist/cmp/AgentGuideContent.d.ts +1 -1
- package/dist/cmp/AgentGuideContent.js +8 -2
- package/dist/cmp/AgentGuideContent.js.map +1 -1
- package/dist/cmp/Feature.js +17 -0
- package/dist/cmp/Feature.js.map +1 -1
- package/dist/cmp/FeatureDocs.d.ts +1 -1
- package/dist/cmp/FeatureDocs.js +6 -1
- package/dist/cmp/FeatureDocs.js.map +1 -1
- package/dist/cmp/ReadmeExplanation.js +3 -1
- package/dist/cmp/ReadmeExplanation.js.map +1 -1
- package/dist/cmp/ReadmeFeatures.js +1 -1
- package/dist/cmp/ReadmeFeatures.js.map +1 -1
- package/dist/cmp/ReadmeRefFeatures.js +1 -1
- package/dist/cmp/ReadmeRefFeatures.js.map +1 -1
- package/dist/helpers/applicability.d.ts +6 -0
- package/dist/helpers/applicability.js +103 -0
- package/dist/helpers/applicability.js.map +1 -0
- package/dist/helpers/collectDeps.js +4 -2
- package/dist/helpers/collectDeps.js.map +1 -1
- package/dist/helpers/serverVars.d.ts +2 -1
- package/dist/helpers/serverVars.js +17 -0
- package/dist/helpers/serverVars.js.map +1 -1
- package/dist/sdkgen.d.ts +3 -2
- package/dist/sdkgen.js +9 -2
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.js +7 -1
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.aon +39 -0
- package/package.json +1 -1
- package/project/.sdk/model/feature/feature-index.aon +1 -0
- package/project/.sdk/model/feature/secrets.aon +118 -0
- package/project/.sdk/model/target/ts.aon +8 -1
- package/project/.sdk/src/cmp/c/Config_c.ts +5 -1
- package/project/.sdk/src/cmp/cpp/Config_cpp.ts +5 -1
- package/project/.sdk/src/cmp/csharp/Config_csharp.ts +5 -1
- package/project/.sdk/src/cmp/dart/Config_dart.ts +5 -1
- package/project/.sdk/src/cmp/elixir/Config_elixir.ts +5 -1
- package/project/.sdk/src/cmp/elixir/Main_elixir.ts +5 -1
- package/project/.sdk/src/cmp/go/Config_go.ts +5 -1
- package/project/.sdk/src/cmp/go/Main_go.ts +6 -2
- package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +16 -3
- package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +16 -3
- package/project/.sdk/src/cmp/go/TestDirect_go.ts +28 -4
- package/project/.sdk/src/cmp/go/TestEntity_go.ts +43 -10
- package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +5 -5
- package/project/.sdk/src/cmp/java/Config_java.ts +22 -1
- package/project/.sdk/src/cmp/java/Main_java.ts +5 -1
- package/project/.sdk/src/cmp/java/ReadmeQuick_java.ts +17 -3
- package/project/.sdk/src/cmp/java/ReadmeTopQuick_java.ts +17 -3
- package/project/.sdk/src/cmp/java/TestDirect_java.ts +23 -4
- package/project/.sdk/src/cmp/java/TestEntity_java.ts +29 -4
- package/project/.sdk/src/cmp/js/Config_js.ts +5 -1
- package/project/.sdk/src/cmp/js/Main_js.ts +5 -1
- package/project/.sdk/src/cmp/js/Package_js.ts +4 -1
- package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +5 -1
- package/project/.sdk/src/cmp/lean/Config_lean.ts +5 -1
- package/project/.sdk/src/cmp/lua/Config_lua.ts +5 -1
- package/project/.sdk/src/cmp/lua/Main_lua.ts +5 -1
- package/project/.sdk/src/cmp/lua/Package_lua.ts +4 -1
- package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +5 -1
- package/project/.sdk/src/cmp/perl/Main_perl.ts +5 -1
- package/project/.sdk/src/cmp/php/Config_php.ts +5 -1
- package/project/.sdk/src/cmp/php/Main_php.ts +5 -1
- package/project/.sdk/src/cmp/py/Config_py.ts +5 -1
- package/project/.sdk/src/cmp/py/Main_py.ts +5 -1
- package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +16 -3
- package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +16 -3
- package/project/.sdk/src/cmp/py/TestDirect_py.ts +24 -5
- package/project/.sdk/src/cmp/py/TestEntity_py.ts +23 -3
- package/project/.sdk/src/cmp/rb/Config_rb.ts +5 -1
- package/project/.sdk/src/cmp/rb/Main_rb.ts +5 -1
- package/project/.sdk/src/cmp/rust/Config_rust.ts +5 -1
- package/project/.sdk/src/cmp/rust/Main_rust.ts +5 -1
- package/project/.sdk/src/cmp/scala/Config_scala.ts +5 -1
- package/project/.sdk/src/cmp/scala/Main_scala.ts +5 -1
- package/project/.sdk/src/cmp/swift/Config_swift.ts +5 -1
- package/project/.sdk/src/cmp/ts/Config_ts.ts +5 -1
- package/project/.sdk/src/cmp/ts/Main_ts.ts +57 -1
- package/project/.sdk/src/cmp/ts/Package_ts.ts +4 -1
- package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +15 -3
- package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +18 -5
- package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +19 -5
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +67 -7
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +27 -4
- package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +9 -2
- package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +9 -2
- package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +7 -0
- package/project/.sdk/src/cmp/zig/Config_zig.ts +5 -1
- package/project/.sdk/tm/c/utility/make_options.c +22 -0
- package/project/.sdk/tm/clojure/src/sdk/core.clj +18 -1
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +23 -0
- package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +15 -0
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +23 -0
- package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +22 -0
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +19 -0
- package/project/.sdk/tm/go/core/context.go +1 -1
- package/project/.sdk/tm/go/feature/cost_feature.go +1 -1
- package/project/.sdk/tm/go/feature/paging_feature.go +5 -5
- package/project/.sdk/tm/go/feature/test_feature.go +12 -5
- package/project/.sdk/tm/go/test/custom_utility_test.go +15 -0
- package/project/.sdk/tm/go/test/feature_corpus_test.go +7 -0
- package/project/.sdk/tm/go/test/runner_test.go +46 -0
- package/project/.sdk/tm/go/test/struct_runner_test.go +7 -7
- package/project/.sdk/tm/go/test/struct_utility_test.go +63 -27
- package/project/.sdk/tm/go/utility/fetcher.go +2 -2
- package/project/.sdk/tm/go/utility/make_options.go +24 -5
- package/project/.sdk/tm/go/utility/make_point.go +1 -1
- package/project/.sdk/tm/go/utility/make_spec.go +17 -1
- package/project/.sdk/tm/go/utility/prepare_auth.go +1 -1
- package/project/.sdk/tm/go/utility/struct/voxgigstruct.go +370 -307
- package/project/.sdk/tm/go/utility/transform_request.go +29 -1
- package/project/.sdk/tm/go/utility/transform_response.go +20 -1
- package/project/.sdk/tm/java/test/CustomUtilityTest.java +10 -0
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +7 -0
- package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +6 -0
- package/project/.sdk/tm/java/test/RunnerSupport.java +43 -0
- package/project/.sdk/tm/java/utility/MakeOptions.java +84 -1
- package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +24 -0
- package/project/.sdk/tm/js/src/utility/StructUtility.js +2076 -1837
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +9 -0
- package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +21 -0
- package/project/.sdk/tm/lean/src/SdkUtility.lean +12 -1
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +11 -0
- package/project/.sdk/tm/perl/utility/make_options.pm +18 -0
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +10 -1
- package/project/.sdk/tm/php/utility/MakeOptions.php +24 -0
- package/project/.sdk/tm/py/pkg/utility/make_options.py +20 -0
- package/project/.sdk/tm/py/test/runner.py +32 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +10 -1
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +9 -1
- package/project/.sdk/tm/rb/utility/make_options.rb +17 -0
- package/project/.sdk/tm/rust/utility/make_options.rs +25 -0
- package/project/.sdk/tm/scala/utility/Make.scala +17 -0
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -0
- package/project/.sdk/tm/ts/src/feature/secrets/SecretsFeature.ts +419 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Providers.ts +1221 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +381 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +151 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +42 -0
- package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +18 -0
- package/project/.sdk/tm/ts/src/utility/StructUtility.ts +1321 -710
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +9 -0
- package/project/.sdk/tm/ts/test/feature/secrets/Secrets.test.ts +764 -0
- package/project/.sdk/tm/ts/test/omni.test.ts +84 -0
- package/project/.sdk/tm/ts/test/omni.ts +86 -0
- package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +6 -2
- package/project/.sdk/tm/ts/test/utility/StructUtility.test.ts +2 -2
- package/project/.sdk/tm/ts/test/utility.ts +122 -0
- package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +692 -0
- package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +238 -0
- package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +188 -0
- package/project/.sdk/tm/ts/test/vendor/omni/index.ts +38 -0
- package/project/.sdk/tm/zig/core/utility.zig +23 -0
- package/project/sdkgen-package.json +2 -1
- package/src/action/check.ts +18 -0
- package/src/cmp/AgentGuide.ts +1 -1
- package/src/cmp/AgentGuideContent.ts +9 -2
- package/src/cmp/Feature.ts +19 -0
- package/src/cmp/FeatureDocs.ts +7 -1
- package/src/cmp/ReadmeExplanation.ts +4 -1
- package/src/cmp/ReadmeFeatures.ts +1 -1
- package/src/cmp/ReadmeRefFeatures.ts +1 -1
- package/src/helpers/applicability.ts +123 -0
- package/src/helpers/collectDeps.ts +5 -1
- package/src/helpers/serverVars.ts +19 -0
- package/src/sdkgen.ts +9 -1
- package/src/utility.ts +10 -1
- 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
|
+
}
|