@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,1221 @@
1
+ // VENDORED: @voxgig/sekreto 0.1.2 (typescript/src/Providers.ts)
2
+ // Source: https://github.com/voxgig/sekreto @ a8c293be1b6c33d65223b2b2275797c241b1a1f1
3
+ // License: MIT (c) voxgig - see repository LICENSE. Do not edit: resync from upstream.
4
+ // The providers a Sekreto chains together.
5
+ //
6
+ // A provider answers one question: "do you have this secret?" It returns
7
+ // the value, or undefined to mean "ask the next one". Nothing else about
8
+ // a provider is visible to the caller - which is the point: an app reads
9
+ // `api.token` and never learns whether it came from the environment, a
10
+ // .env file, HashiCorp Vault, AWS, GCP, Azure or a boru vault.
11
+ //
12
+ // Two failure shapes, and they are never interchangeable. A store that
13
+ // does not hold the secret is a MISS (undefined) - the chain carries on.
14
+ // A store that could not answer - bad credentials, unreachable host,
15
+ // missing configuration - is an ERROR: falling through there would
16
+ // quietly reach for a weaker store.
17
+
18
+ import { spawnSync } from 'node:child_process'
19
+ import { readFileSync } from 'node:fs'
20
+ import { join } from 'node:path'
21
+
22
+ import {
23
+ SekretoError,
24
+ awsparam,
25
+ checkname,
26
+ envkey,
27
+ flatname,
28
+ parsedotenv,
29
+ vaultref,
30
+ } from './Sekreto'
31
+ import { sigv4 } from './Sigv4'
32
+
33
+ export type Provider = {
34
+ /** The value, or undefined if this provider does not have it. */
35
+ lookup: (name: string) => Promise<string | undefined> | string | undefined
36
+ /** A short description, shown by `Sekreto.sources()`. */
37
+ describe: () => string
38
+ }
39
+
40
+ /** The declarative form of a provider, as used in config and in the
41
+ * shared spec. */
42
+ export type ProviderSpec = {
43
+ kind:
44
+ | 'env'
45
+ | 'dotenv'
46
+ | 'memory'
47
+ | 'file'
48
+ | 'hashicorp'
49
+ | 'boru'
50
+ | 'awssecrets'
51
+ | 'awsparams'
52
+ | 'gcpsecrets'
53
+ | 'azuresecrets'
54
+ | 'onepassword'
55
+ | 'doppler'
56
+ | 'infisical'
57
+ /** The store name `Sekreto.getfrom` addresses. Defaults to `kind`. */
58
+ name?: string
59
+ prefix?: string
60
+ /** dotenv: the file to read. */
61
+ file?: string
62
+ /** memory: literal values, keyed like environment variables. */
63
+ values?: Record<string, string>
64
+ /** file: the directory of one-secret-per-file entries. */
65
+ dir?: string
66
+ /** hashicorp / boru (wire) / gcp / 1password / doppler / infisical:
67
+ * the base URL. */
68
+ addr?: string
69
+ /** hashicorp / boru (wire) / gcp / azure / 1password / doppler /
70
+ * infisical: the access token. */
71
+ token?: string
72
+ /** hashicorp / boru (wire): the KV mount (default `secret`). */
73
+ mount?: string
74
+ /** hashicorp: KV engine version, 1 or 2 (default 2). */
75
+ kv?: number
76
+ /** hashicorp: Vault Enterprise namespace (X-Vault-Namespace). */
77
+ vaultnamespace?: string
78
+ /** hashicorp: log in for a token instead of being handed one. */
79
+ auth?: {
80
+ method: 'kubernetes' | 'approle'
81
+ /** The auth mount, defaulting to the method name. */
82
+ mount?: string
83
+ /** kubernetes: the Vault role to log in as. */
84
+ role?: string
85
+ /** kubernetes: the service-account JWT itself (tests). */
86
+ jwt?: string
87
+ /** kubernetes: where the JWT lives; the conventional pod path by
88
+ * default. */
89
+ jwtfile?: string
90
+ /** approle: the role and secret ids. */
91
+ roleid?: string
92
+ secretid?: string
93
+ }
94
+ /** boru: the executable to run (default `boru`). */
95
+ command?: string
96
+ /** boru: the namespace qualifying the alias. */
97
+ namespace?: string
98
+ /** boru: the vault home, passed as BORU_HOME. */
99
+ home?: string
100
+ /** aws: region and credentials; the standard AWS_* environment
101
+ * variables fill whichever are not given. */
102
+ region?: string
103
+ keyid?: string
104
+ secret?: string
105
+ session?: string
106
+ /** gcp / doppler / infisical: the project (GCP project id, Doppler
107
+ * project slug, Infisical workspace id). */
108
+ project?: string
109
+ /** azure: the Key Vault name or full URL. 1password: the vault name
110
+ * or id. */
111
+ vault?: string
112
+ /** azure: client-credential login. infisical: universal-auth login
113
+ * (tenant is Azure-only). */
114
+ tenant?: string
115
+ clientid?: string
116
+ clientsecret?: string
117
+ /** azure: where to log in / where IMDS answers. gcp: where the
118
+ * metadata server answers. Overridable for tests and for clouds with
119
+ * nonstandard endpoints. */
120
+ loginaddr?: string
121
+ imdsaddr?: string
122
+ metadataaddr?: string
123
+ /** azure: the Key Vault API version (default 7.4). */
124
+ apiversion?: string
125
+ /** doppler: the config slug (with `project`). */
126
+ config?: string
127
+ /** infisical: the environment slug and secret path. */
128
+ environment?: string
129
+ path?: string
130
+ }
131
+
132
+ /** Environment variables: `api.token` from `API_TOKEN`. */
133
+ export function envprovider(prefix?: string, source?: Record<string, any>): Provider {
134
+ const env = source || process.env
135
+
136
+ return {
137
+ lookup: (name: string) => {
138
+ const value = env[envkey(name, prefix)]
139
+ return undefined === value || null === value ? undefined : String(value)
140
+ },
141
+ describe: () => 'env' + (prefix ? ':' + prefix : ''),
142
+ }
143
+ }
144
+
145
+ /** A `.env` file, read once, keyed exactly like the environment. */
146
+ export function dotenvprovider(file: string, prefix?: string): Provider {
147
+ let values: Record<string, string> | undefined
148
+
149
+ const load = () => {
150
+ if (undefined === values) {
151
+ try {
152
+ values = parsedotenv(readFileSync(file, 'utf8'))
153
+ } catch (err: any) {
154
+ // An absent file - or an absent directory - means "no secrets
155
+ // here", exactly like fileprovider. Anything else (permission
156
+ // denied, an unreadable mount) is a store that could not answer,
157
+ // and swallowing it would fall through to a weaker store.
158
+ if ('ENOENT' === err.code || 'ENOTDIR' === err.code) {
159
+ values = {}
160
+ } else {
161
+ throw new SekretoError(
162
+ 'sekreto: dotenv provider cannot read ' + file + ': ' + err.message,
163
+ )
164
+ }
165
+ }
166
+ }
167
+ return values
168
+ }
169
+
170
+ return {
171
+ lookup: (name: string) => load()[envkey(name, prefix)],
172
+ describe: () => 'dotenv:' + file,
173
+ }
174
+ }
175
+
176
+ /** Literal values, keyed like environment variables. The spec uses this
177
+ * to test chain behaviour without touching the outside world. */
178
+ export function memoryprovider(values: Record<string, string>, prefix?: string): Provider {
179
+ return {
180
+ lookup: (name: string) => values[envkey(name, prefix)],
181
+ describe: () => 'memory' + (prefix ? ':' + prefix : ''),
182
+ }
183
+ }
184
+
185
+ /** A directory of one-secret-per-file entries, keyed like the
186
+ * environment: `api.token` reads `<dir>/API_TOKEN`.
187
+ *
188
+ * This is the shape of a mounted Kubernetes Secret, a Docker or Swarm
189
+ * secret, and a systemd credentials directory, so those all work with no
190
+ * further configuration. One trailing newline is stripped - tools that
191
+ * write these files disagree about it, and a newline is never part of a
192
+ * secret on purpose. */
193
+ export function fileprovider(dir: string, prefix?: string): Provider {
194
+ return {
195
+ lookup: (name: string) => {
196
+ const file = join(dir, envkey(name, prefix))
197
+
198
+ let text: string
199
+ try {
200
+ text = readFileSync(file, 'utf8')
201
+ } catch (err: any) {
202
+ // An absent file - or an absent directory - means "no secrets
203
+ // here", exactly like a missing .env. Anything else (permission
204
+ // denied, an unreadable mount) is a store that could not answer.
205
+ if ('ENOENT' === err.code || 'ENOTDIR' === err.code) {
206
+ return undefined
207
+ }
208
+ throw new SekretoError('sekreto: file provider cannot read ' + file + ': ' + err.message)
209
+ }
210
+
211
+ return text.replace(/\r?\n$/, '')
212
+ },
213
+ describe: () => 'file:' + dir,
214
+ }
215
+ }
216
+
217
+ /** Refuse to send a secret-bearing credential in the clear.
218
+ *
219
+ * A vault API is HTTPS in any real deployment; plaintext is a dev-mode
220
+ * convenience. Sending a token over http to anything but the local
221
+ * machine puts both the token and the secret it fetches on the wire for
222
+ * anyone on the path, so sekreto will not do it. Loopback stays allowed:
223
+ * that is `vault server -dev`, `boru vault serve`, and this repo's own
224
+ * test harness. */
225
+ export function checkaddr(addr: string): void {
226
+ if (addr.startsWith('https://')) {
227
+ return
228
+ }
229
+
230
+ if (!addr.startsWith('http://')) {
231
+ throw new SekretoError('sekreto: not an http(s) address: ' + addr)
232
+ }
233
+
234
+ const host = addr.slice('http://'.length).split('/')[0].split(':')[0]
235
+
236
+ if ('localhost' === host || '127.0.0.1' === host || '::1' === host || '[::1]' === host) {
237
+ return
238
+ }
239
+
240
+ throw new SekretoError(
241
+ 'sekreto: refusing to send a token in plaintext to ' + addr + ' (use https)',
242
+ )
243
+ }
244
+
245
+ /** How long any single vault round-trip may take before it is treated as
246
+ * unreachable. Ports carry the same bound. */
247
+ const HTTP_TIMEOUT_MS = 10000
248
+
249
+ /** One JSON round-trip. Network failure is always an error - an
250
+ * unreachable store is a store that could not answer. */
251
+ async function fetchjson(
252
+ method: string,
253
+ url: string,
254
+ headers: Record<string, string>,
255
+ body?: string,
256
+ ): Promise<{ status: number; body: any }> {
257
+ let res: Response
258
+ try {
259
+ res = await fetch(url, {
260
+ method,
261
+ headers,
262
+ body,
263
+ // A vault API never legitimately redirects, and a followed redirect
264
+ // carries X-Vault-Token to the redirect's host (and can downgrade
265
+ // https to http), which checkaddr - it only validates the configured
266
+ // address - cannot see. Refuse to follow one.
267
+ redirect: 'error',
268
+ // Bound the wait so an accepted-but-silent endpoint cannot hang the
269
+ // caller (and the app's startup) forever.
270
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
271
+ })
272
+ } catch (err: any) {
273
+ throw new SekretoError('sekreto: cannot reach ' + url.split('?')[0] + ': ' + err.message)
274
+ }
275
+
276
+ let parsed: any = undefined
277
+ try {
278
+ parsed = await res.json()
279
+ } catch (err: any) {
280
+ // A success status promised JSON; a body that does not parse means
281
+ // the store could not answer coherently, and treating it as a miss
282
+ // would fall through to a weaker store. Error statuses may carry
283
+ // any body - they are decided on status alone.
284
+ if (200 === res.status) {
285
+ throw new SekretoError('sekreto: malformed response from ' + url.split('?')[0])
286
+ }
287
+ }
288
+
289
+ return { status: res.status, body: parsed }
290
+ }
291
+
292
+ /** HashiCorp Vault.
293
+ *
294
+ * KV v2 (the default): `api.token` reads `{addr}/v1/{mount}/data/api`
295
+ * and takes the `token` field of `data.data`. KV v1 (`kv: 1`) reads
296
+ * `{addr}/v1/{mount}/api` and takes the field of `data`. A 404 means
297
+ * "not here" - a miss - so a vault can sit in a chain with fallbacks.
298
+ *
299
+ * A Vault Enterprise namespace rides the X-Vault-Namespace header, on
300
+ * logins as well as reads.
301
+ *
302
+ * Instead of being handed a token, the provider can log in: Kubernetes
303
+ * auth (the pod's service-account JWT, from its conventional path) or
304
+ * AppRole. A failed login is an error, never a miss - it means this
305
+ * store could not answer at all. */
306
+ export function hashicorpprovider(
307
+ addr: string,
308
+ token: string,
309
+ options?: {
310
+ mount?: string
311
+ kv?: number
312
+ vaultnamespace?: string
313
+ auth?: ProviderSpec['auth']
314
+ },
315
+ ): Provider {
316
+ const opts = options || {}
317
+ const usemount = opts.mount || 'secret'
318
+ const kv = opts.kv || 2
319
+
320
+ // A version typo like kv: 3 must not quietly behave as v2 and turn
321
+ // its 404s into misses; there is nothing safe to assume it meant.
322
+ if (1 !== kv && 2 !== kv) {
323
+ throw new SekretoError('sekreto: hashicorp: unsupported kv version: ' + String(kv))
324
+ }
325
+
326
+ // The working token: a configured token is kept forever, a logged-in
327
+ // token is renewed shortly before its lease runs out - a long-running
328
+ // process must not keep presenting a token the vault already expired.
329
+ let livetoken: string | undefined = '' === token ? undefined : token
330
+ let renewat = Infinity
331
+
332
+ const baseheaders = (): Record<string, string> => {
333
+ const headers: Record<string, string> = {}
334
+ if (opts.vaultnamespace) {
335
+ headers['X-Vault-Namespace'] = opts.vaultnamespace
336
+ }
337
+ return headers
338
+ }
339
+
340
+ const login = async (): Promise<string> => {
341
+ const auth = opts.auth
342
+ if (!auth) {
343
+ throw new SekretoError('sekreto: hashicorp: no token and no auth method')
344
+ }
345
+
346
+ const mount = auth.mount || auth.method
347
+ const url = addr.replace(/\/$/, '') + '/v1/auth/' + mount + '/login'
348
+
349
+ let body: any
350
+ if ('kubernetes' === auth.method) {
351
+ let jwt = auth.jwt
352
+ if (undefined === jwt) {
353
+ const file = auth.jwtfile || '/var/run/secrets/kubernetes.io/serviceaccount/token'
354
+ try {
355
+ jwt = readFileSync(file, 'utf8').trim()
356
+ } catch (err: any) {
357
+ throw new SekretoError('sekreto: hashicorp: cannot read jwt file ' + file)
358
+ }
359
+ }
360
+ body = { role: auth.role || '', jwt }
361
+ } else if ('approle' === auth.method) {
362
+ body = { role_id: auth.roleid || '', secret_id: auth.secretid || '' }
363
+ } else {
364
+ throw new SekretoError('sekreto: hashicorp: unknown auth method: ' + String(auth.method))
365
+ }
366
+
367
+ const res = await fetchjson('POST', url, baseheaders(), JSON.stringify(body))
368
+
369
+ const got = res.body && res.body.auth && res.body.auth.client_token
370
+ if (200 !== res.status || !got) {
371
+ throw new SekretoError('sekreto: hashicorp login failed: ' + res.status + ': ' + url)
372
+ }
373
+
374
+ const lease = Number(res.body.auth.lease_duration)
375
+ renewat = 0 < lease ? Date.now() + Math.max(lease - 60, 1) * 1000 : Infinity
376
+
377
+ return String(got)
378
+ }
379
+
380
+ return {
381
+ lookup: async (name: string) => {
382
+ checkaddr(addr)
383
+
384
+ if (undefined === livetoken || Date.now() >= renewat) {
385
+ livetoken = await login()
386
+ }
387
+
388
+ const ref = vaultref(name)
389
+ const base = addr.replace(/\/$/, '') + '/v1/' + usemount
390
+ const url = 1 === kv ? base + '/' + ref.path : base + '/data/' + ref.path
391
+
392
+ const headers = baseheaders()
393
+ headers['X-Vault-Token'] = livetoken
394
+
395
+ const res = await fetchjson('GET', url, headers)
396
+
397
+ if (404 === res.status) {
398
+ return undefined
399
+ }
400
+
401
+ if (200 !== res.status) {
402
+ throw new SekretoError('sekreto: hashicorp error: ' + res.status + ': ' + url)
403
+ }
404
+
405
+ const data =
406
+ 1 === kv ? res.body && res.body.data : res.body && res.body.data && res.body.data.data
407
+
408
+ const value = data ? data[ref.field] : undefined
409
+ return undefined === value || null === value ? undefined : String(value)
410
+ },
411
+ describe: () => 'hashicorp:' + addr + '/' + usemount,
412
+ }
413
+ }
414
+
415
+ /** A boru vault (https://github.com/boru-lang/boru).
416
+ *
417
+ * Two ways in, both boru's own.
418
+ *
419
+ * With no `addr`, the CLI: `boru vault get --reveal <alias>` prints the
420
+ * secret on stdout and nothing else. The passphrase is read by boru
421
+ * itself from `BORU_VAULT_PASSPHRASE`; sekreto never accepts it as
422
+ * config and never puts it on a command line, where it would show up in
423
+ * the process table.
424
+ *
425
+ * With an `addr`, boru's wire protocol: `boru vault serve` publishes a
426
+ * read-only, HashiCorp-shaped provision API (boru's
427
+ * design/VAULT-WIRE-PROTOCOL.0.md), authenticated by a capability token
428
+ * from `boru vault grant`. A sekreto name is already a valid boru
429
+ * alias, and boru aliases keep their dots, so `api.token` is the single
430
+ * path segment `api.token` - not the `api`/`token` split a HashiCorp KV
431
+ * gets. The value is the `value` field. A 404 is a miss; anything else
432
+ * the server refuses (a revoked capability, a sealed vault) is an
433
+ * error.
434
+ *
435
+ * boru's `vault proxy` and `vault mcp` remain out of bounds: they are a
436
+ * credential *broker*, built precisely so the caller never receives the
437
+ * credential. `vault serve` is the provision endpoint, built to hand
438
+ * the value back - that is the one sekreto uses. */
439
+ export function boruprovider(options?: {
440
+ command?: string
441
+ namespace?: string
442
+ home?: string
443
+ addr?: string
444
+ token?: string
445
+ mount?: string
446
+ }): Provider {
447
+ const opts = options || {}
448
+ const command = opts.command || 'boru'
449
+
450
+ if (opts.addr) {
451
+ const addr = opts.addr.replace(/\/$/, '')
452
+ const mount = opts.mount || 'secret'
453
+
454
+ return {
455
+ lookup: async (name: string) => {
456
+ checkname(name)
457
+ checkaddr(addr)
458
+
459
+ const alias = opts.namespace ? opts.namespace + '/' + name : name
460
+ const url = addr + '/v1/' + mount + '/data/' + alias
461
+
462
+ const res = await fetchjson('GET', url, { 'X-Vault-Token': opts.token || '' })
463
+
464
+ if (404 === res.status) {
465
+ return undefined
466
+ }
467
+
468
+ if (200 !== res.status) {
469
+ throw new SekretoError('sekreto: boru serve error: ' + res.status + ': ' + url)
470
+ }
471
+
472
+ const data = res.body && res.body.data && res.body.data.data
473
+ const value = data ? data['value'] : undefined
474
+ return undefined === value || null === value ? undefined : String(value)
475
+ },
476
+ describe: () => 'boru:' + addr,
477
+ }
478
+ }
479
+
480
+ return {
481
+ lookup: (name: string) => {
482
+ checkname(name)
483
+
484
+ const alias = opts.namespace ? opts.namespace + ':' + name : name
485
+ const env = opts.home ? { ...process.env, BORU_HOME: opts.home } : process.env
486
+
487
+ const run = spawnSync(command, ['vault', 'get', '--reveal', alias], {
488
+ encoding: 'utf8',
489
+ env,
490
+ })
491
+
492
+ if (run.error) {
493
+ throw new SekretoError('sekreto: cannot run ' + command + ': ' + run.error.message)
494
+ }
495
+
496
+ if (0 === run.status) {
497
+ // boru prints the value and one newline, and nothing else.
498
+ return run.stdout.replace(/\n$/, '')
499
+ }
500
+
501
+ const why = (run.stderr || '').trim()
502
+
503
+ // "no alias named" is boru saying it does not hold this secret, which
504
+ // is a miss: the chain carries on to the next provider. A locked vault
505
+ // or a wrong passphrase is not a miss - treating it as one would fall
506
+ // through to a weaker store without saying so.
507
+ if (borumiss(why)) {
508
+ return undefined
509
+ }
510
+
511
+ throw new SekretoError('sekreto: boru vault error: ' + (why || 'exit ' + run.status))
512
+ },
513
+ describe: () => 'boru' + (opts.namespace ? ':' + opts.namespace : ''),
514
+ }
515
+ }
516
+
517
+ /** Does this boru failure mean "no such secret" rather than "I could not
518
+ * answer"? Matched on boru's own wording for a missing alias. */
519
+ function borumiss(why: string): boolean {
520
+ return /no alias named/.test(why)
521
+ }
522
+
523
+ /** The `YYYYMMDDTHHMMSSZ` timestamp SigV4 wants, for now. */
524
+ function awsnow(): string {
525
+ return new Date()
526
+ .toISOString()
527
+ .replace(/[-:]/g, '')
528
+ .replace(/\.\d+Z$/, 'Z')
529
+ }
530
+
531
+ type Awsopts = {
532
+ region?: string
533
+ keyid?: string
534
+ secret?: string
535
+ session?: string
536
+ addr?: string
537
+ prefix?: string
538
+ }
539
+
540
+ /** Region and credentials, from config first and the standard AWS_*
541
+ * environment variables second - those are AWS's own convention, and a
542
+ * pod or CI job that has them set should just work. Missing either is
543
+ * an error: an AWS store with no credentials could not answer. */
544
+ function awsauth(opts: Awsopts): {
545
+ region: string
546
+ keyid: string
547
+ secret: string
548
+ session?: string
549
+ } {
550
+ const env = process.env
551
+
552
+ const region = opts.region || env.AWS_REGION || env.AWS_DEFAULT_REGION || ''
553
+ const keyid = opts.keyid || env.AWS_ACCESS_KEY_ID || ''
554
+ const secret = opts.secret || env.AWS_SECRET_ACCESS_KEY || ''
555
+ const session = opts.session || env.AWS_SESSION_TOKEN || undefined
556
+
557
+ if ('' === region) {
558
+ throw new SekretoError('sekreto: aws: no region (set region or AWS_REGION)')
559
+ }
560
+ if ('' === keyid || '' === secret) {
561
+ throw new SekretoError(
562
+ 'sekreto: aws: no credentials (set keyid/secret or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY)',
563
+ )
564
+ }
565
+
566
+ return { region, keyid, secret, session }
567
+ }
568
+
569
+ /** One signed call to an AWS JSON-1.1 API. */
570
+ async function awscall(
571
+ opts: Awsopts,
572
+ service: string,
573
+ target: string,
574
+ payload: any,
575
+ ): Promise<{ status: number; body: any }> {
576
+ const auth = awsauth(opts)
577
+ // The China partition lives under its own suffix; every other
578
+ // commercial region is plain amazonaws.com.
579
+ const suffix = auth.region.startsWith('cn-') ? '.amazonaws.com.cn' : '.amazonaws.com'
580
+ const addr = opts.addr || 'https://' + service + '.' + auth.region + suffix
581
+ checkaddr(addr)
582
+
583
+ const url = addr.replace(/\/$/, '') + '/'
584
+ const body = JSON.stringify(payload)
585
+ const headers: Record<string, string> = {
586
+ 'content-type': 'application/x-amz-json-1.1',
587
+ 'x-amz-target': target,
588
+ }
589
+
590
+ const signed = sigv4({
591
+ method: 'POST',
592
+ url,
593
+ headers,
594
+ body,
595
+ service,
596
+ region: auth.region,
597
+ keyid: auth.keyid,
598
+ secret: auth.secret,
599
+ session: auth.session,
600
+ datetime: awsnow(),
601
+ })
602
+
603
+ return fetchjson('POST', url, { ...headers, ...signed }, body)
604
+ }
605
+
606
+ /** Does this AWS error body name one of the not-found types? Those are
607
+ * a miss; every other failure is a store that could not answer. */
608
+ function awsmiss(body: any, types: string[]): boolean {
609
+ const errtype = body && 'string' === typeof body.__type ? body.__type : ''
610
+ return types.some((name) => errtype.includes(name))
611
+ }
612
+
613
+ /** AWS Secrets Manager.
614
+ *
615
+ * `api.token` reads the secret named `api` (the vaultref path, so
616
+ * `db.pass.main` reads `db/pass`) and takes the `token` field of its
617
+ * JSON SecretString - the AWS idiom of one JSON map per secret. A
618
+ * SecretString that is not JSON is the value itself, under the
619
+ * conventional field `value`. Requests are SigV4-signed in-tree; see
620
+ * Sigv4.ts. */
621
+ export function awssecretsprovider(options?: Awsopts): Provider {
622
+ const opts = options || {}
623
+
624
+ return {
625
+ lookup: async (name: string) => {
626
+ const ref = vaultref(name)
627
+
628
+ const res = await awscall(opts, 'secretsmanager', 'secretsmanager.GetSecretValue', {
629
+ SecretId: ref.path,
630
+ })
631
+
632
+ if (400 === res.status && awsmiss(res.body, ['ResourceNotFoundException'])) {
633
+ return undefined
634
+ }
635
+
636
+ if (200 !== res.status) {
637
+ throw new SekretoError('sekreto: aws secretsmanager error: ' + res.status)
638
+ }
639
+
640
+ const text = res.body && res.body.SecretString
641
+
642
+ if ('string' !== typeof text) {
643
+ // A binary secret has no fields to address; only the conventional
644
+ // `value` field can mean "the bytes themselves".
645
+ const bin = res.body && res.body.SecretBinary
646
+ if ('string' === typeof bin && 'value' === ref.field) {
647
+ return Buffer.from(bin, 'base64').toString('utf8')
648
+ }
649
+ return undefined
650
+ }
651
+
652
+ let parsed: any
653
+ try {
654
+ parsed = JSON.parse(text)
655
+ } catch (err: any) {
656
+ parsed = undefined
657
+ }
658
+
659
+ if (parsed && 'object' === typeof parsed && !Array.isArray(parsed)) {
660
+ const value = parsed[ref.field]
661
+ return undefined === value || null === value ? undefined : String(value)
662
+ }
663
+
664
+ // A plain-string secret is the whole value; it has no named fields.
665
+ return 'value' === ref.field ? text : undefined
666
+ },
667
+ // Config only, never the environment: describe() feeds the spec's
668
+ // sources group, which must answer the same everywhere.
669
+ describe: () => 'awssecrets:' + (opts.region || ''),
670
+ }
671
+ }
672
+
673
+ /** AWS SSM Parameter Store.
674
+ *
675
+ * `db.pass.main` reads the parameter `/db/pass/main` (under an optional
676
+ * prefix path), decrypted. Parameter Store carries flat strings, so
677
+ * there is no field indirection. */
678
+ export function awsparamsprovider(options?: Awsopts): Provider {
679
+ const opts = options || {}
680
+
681
+ return {
682
+ lookup: async (name: string) => {
683
+ const res = await awscall(opts, 'ssm', 'AmazonSSM.GetParameter', {
684
+ Name: awsparam(name, opts.prefix),
685
+ WithDecryption: true,
686
+ })
687
+
688
+ if (400 === res.status && awsmiss(res.body, ['ParameterNotFound'])) {
689
+ return undefined
690
+ }
691
+
692
+ if (200 !== res.status) {
693
+ throw new SekretoError('sekreto: aws ssm error: ' + res.status)
694
+ }
695
+
696
+ const value = res.body && res.body.Parameter && res.body.Parameter.Value
697
+ return undefined === value || null === value ? undefined : String(value)
698
+ },
699
+ describe: () => 'awsparams:' + (opts.region || '') + (opts.prefix || ''),
700
+ }
701
+ }
702
+
703
+ /** GCP Secret Manager.
704
+ *
705
+ * `api.token` reads secret `api_token` (dots flattened to `_`; Secret
706
+ * Manager ids have no hierarchy and reject dots), latest version. The
707
+ * token comes from config, then `GOOGLE_OAUTH_ACCESS_TOKEN`, then the
708
+ * GCE/GKE metadata server - so on Google's own platform no credential
709
+ * configuration is needed at all.
710
+ *
711
+ * The metadata call itself is plain http to a link-local host by
712
+ * platform design; no credential rides on it, so `checkaddr` guards the
713
+ * Secret Manager address instead. */
714
+ export function gcpsecretsprovider(options?: {
715
+ project?: string
716
+ token?: string
717
+ addr?: string
718
+ metadataaddr?: string
719
+ }): Provider {
720
+ const opts = options || {}
721
+
722
+ // A configured token is kept forever; a metadata-server token carries
723
+ // expires_in and is renewed shortly before it runs out.
724
+ let livetoken: string | undefined
725
+ let renewat = Infinity
726
+
727
+ const metadataaddr = (): string => {
728
+ if (opts.metadataaddr) {
729
+ return opts.metadataaddr
730
+ }
731
+ const host = process.env.GCE_METADATA_HOST
732
+ return host ? 'http://' + host : 'http://metadata.google.internal'
733
+ }
734
+
735
+ const login = async (): Promise<string> => {
736
+ const configured = opts.token || process.env.GOOGLE_OAUTH_ACCESS_TOKEN
737
+ if (configured) {
738
+ return configured
739
+ }
740
+
741
+ const url =
742
+ metadataaddr().replace(/\/$/, '') +
743
+ '/computeMetadata/v1/instance/service-accounts/default/token'
744
+
745
+ const res = await fetchjson('GET', url, { 'Metadata-Flavor': 'Google' })
746
+
747
+ const got = res.body && res.body.access_token
748
+ if (200 !== res.status || !got) {
749
+ throw new SekretoError('sekreto: gcp: no token and metadata server did not answer')
750
+ }
751
+
752
+ const expires = Number(res.body.expires_in)
753
+ renewat = 0 < expires ? Date.now() + Math.max(expires - 60, 1) * 1000 : Infinity
754
+
755
+ return String(got)
756
+ }
757
+
758
+ return {
759
+ lookup: async (name: string) => {
760
+ const project = opts.project || ''
761
+ if ('' === project) {
762
+ throw new SekretoError('sekreto: gcp: no project')
763
+ }
764
+
765
+ const addr = opts.addr || 'https://secretmanager.googleapis.com'
766
+ checkaddr(addr)
767
+
768
+ if (undefined === livetoken || Date.now() >= renewat) {
769
+ livetoken = await login()
770
+ }
771
+
772
+ const url =
773
+ addr.replace(/\/$/, '') +
774
+ '/v1/projects/' +
775
+ project +
776
+ '/secrets/' +
777
+ flatname(name, '_') +
778
+ '/versions/latest:access'
779
+
780
+ const res = await fetchjson('GET', url, { authorization: 'Bearer ' + livetoken })
781
+
782
+ if (404 === res.status) {
783
+ return undefined
784
+ }
785
+
786
+ if (200 !== res.status) {
787
+ throw new SekretoError('sekreto: gcp error: ' + res.status + ': ' + url)
788
+ }
789
+
790
+ const data = res.body && res.body.payload && res.body.payload.data
791
+ if ('string' !== typeof data) {
792
+ return undefined
793
+ }
794
+
795
+ return Buffer.from(data, 'base64').toString('utf8')
796
+ },
797
+ describe: () => 'gcpsecrets:' + (opts.project || ''),
798
+ }
799
+ }
800
+
801
+ /** Azure Key Vault.
802
+ *
803
+ * `api.token` reads secret `api-token` (dots flattened to `-`; Key
804
+ * Vault names allow nothing else), current version. The token comes
805
+ * from config, then a client-credentials login when tenant/clientid/
806
+ * clientsecret are given, then the IMDS managed-identity endpoint - so
807
+ * on Azure's own platform no credential configuration is needed.
808
+ *
809
+ * As with GCP, the IMDS call is plain http to a link-local host by
810
+ * platform design and carries no credential; the login and vault
811
+ * addresses are `checkaddr`-guarded. */
812
+ export function azuresecretsprovider(options?: {
813
+ vault?: string
814
+ token?: string
815
+ tenant?: string
816
+ clientid?: string
817
+ clientsecret?: string
818
+ loginaddr?: string
819
+ imdsaddr?: string
820
+ apiversion?: string
821
+ }): Provider {
822
+ const opts = options || {}
823
+ const resource = 'https://vault.azure.net'
824
+
825
+ // A configured token is kept forever; logged-in and IMDS tokens carry
826
+ // expires_in and are renewed shortly before they run out.
827
+ let livetoken: string | undefined
828
+ let renewat = Infinity
829
+
830
+ const expiry = (expires: any): number => {
831
+ const seconds = Number(expires)
832
+ return 0 < seconds ? Date.now() + Math.max(seconds - 60, 1) * 1000 : Infinity
833
+ }
834
+
835
+ const login = async (): Promise<string> => {
836
+ if (opts.token) {
837
+ return opts.token
838
+ }
839
+
840
+ if (opts.tenant && opts.clientid && opts.clientsecret) {
841
+ const loginaddr = opts.loginaddr || 'https://login.microsoftonline.com'
842
+ checkaddr(loginaddr)
843
+
844
+ const url = loginaddr.replace(/\/$/, '') + '/' + opts.tenant + '/oauth2/v2.0/token'
845
+ const form =
846
+ 'grant_type=client_credentials&client_id=' +
847
+ encodeURIComponent(opts.clientid) +
848
+ '&client_secret=' +
849
+ encodeURIComponent(opts.clientsecret) +
850
+ '&scope=' +
851
+ encodeURIComponent(resource + '/.default')
852
+
853
+ const res = await fetchjson(
854
+ 'POST',
855
+ url,
856
+ { 'content-type': 'application/x-www-form-urlencoded' },
857
+ form,
858
+ )
859
+
860
+ const got = res.body && res.body.access_token
861
+ if (200 !== res.status || !got) {
862
+ throw new SekretoError('sekreto: azure login failed: ' + res.status)
863
+ }
864
+
865
+ renewat = expiry(res.body.expires_in)
866
+ return String(got)
867
+ }
868
+
869
+ const imds =
870
+ (opts.imdsaddr || 'http://169.254.169.254').replace(/\/$/, '') +
871
+ '/metadata/identity/oauth2/token?api-version=2018-02-01&resource=' +
872
+ encodeURIComponent(resource)
873
+
874
+ const res = await fetchjson('GET', imds, { Metadata: 'true' })
875
+
876
+ const got = res.body && res.body.access_token
877
+ if (200 !== res.status || !got) {
878
+ throw new SekretoError(
879
+ 'sekreto: azure: no token, no client credentials, and IMDS did not answer',
880
+ )
881
+ }
882
+
883
+ renewat = expiry(res.body.expires_in)
884
+ return String(got)
885
+ }
886
+
887
+ return {
888
+ lookup: async (name: string) => {
889
+ const vault = opts.vault || ''
890
+ if ('' === vault) {
891
+ throw new SekretoError('sekreto: azure: no vault')
892
+ }
893
+
894
+ // Only an explicit scheme is a URL; a vault NAMED httpvault must
895
+ // still become https://httpvault.vault.azure.net.
896
+ const vaulturl =
897
+ vault.startsWith('http://') || vault.startsWith('https://')
898
+ ? vault
899
+ : 'https://' + vault + '.vault.azure.net'
900
+ checkaddr(vaulturl)
901
+
902
+ if (undefined === livetoken || Date.now() >= renewat) {
903
+ livetoken = await login()
904
+ }
905
+
906
+ const url =
907
+ vaulturl.replace(/\/$/, '') +
908
+ '/secrets/' +
909
+ flatname(name, '-') +
910
+ '?api-version=' +
911
+ (opts.apiversion || '7.4')
912
+
913
+ const res = await fetchjson('GET', url, { authorization: 'Bearer ' + livetoken })
914
+
915
+ if (404 === res.status) {
916
+ return undefined
917
+ }
918
+
919
+ if (200 !== res.status) {
920
+ throw new SekretoError('sekreto: azure error: ' + res.status + ': ' + url.split('?')[0])
921
+ }
922
+
923
+ const value = res.body && res.body.value
924
+ return undefined === value || null === value ? undefined : String(value)
925
+ },
926
+ describe: () => 'azuresecrets:' + (opts.vault || ''),
927
+ }
928
+ }
929
+
930
+ /** 1Password, through a Connect server.
931
+ *
932
+ * The item titled `api.token` (titles keep their dots), in the named
933
+ * vault. The value is the field with purpose PASSWORD, or the field
934
+ * labelled `value`. A vault that cannot be found is an error - config
935
+ * names it, so its absence is a broken store, not a missing secret. */
936
+ export function onepasswordprovider(options?: {
937
+ addr?: string
938
+ token?: string
939
+ vault?: string
940
+ }): Provider {
941
+ const opts = options || {}
942
+
943
+ let vaultid: string | undefined
944
+
945
+ const auth = (): Record<string, string> => ({
946
+ authorization: 'Bearer ' + (opts.token || ''),
947
+ })
948
+
949
+ const resolvevault = async (addr: string): Promise<string> => {
950
+ const want = opts.vault || ''
951
+ if ('' === want) {
952
+ throw new SekretoError('sekreto: onepassword: no vault')
953
+ }
954
+
955
+ const res = await fetchjson('GET', addr + '/v1/vaults', auth())
956
+
957
+ if (200 !== res.status || !Array.isArray(res.body)) {
958
+ throw new SekretoError('sekreto: onepassword error: ' + res.status + ': listing vaults')
959
+ }
960
+
961
+ for (const entry of res.body) {
962
+ if (entry && (want === entry.id || want === entry.name)) {
963
+ return String(entry.id)
964
+ }
965
+ }
966
+
967
+ throw new SekretoError('sekreto: onepassword: no vault named ' + want)
968
+ }
969
+
970
+ return {
971
+ lookup: async (name: string) => {
972
+ checkname(name)
973
+
974
+ const addr = (opts.addr || '').replace(/\/$/, '')
975
+ if ('' === addr) {
976
+ throw new SekretoError('sekreto: onepassword: no addr')
977
+ }
978
+ checkaddr(addr)
979
+
980
+ if (undefined === vaultid) {
981
+ vaultid = await resolvevault(addr)
982
+ }
983
+
984
+ const filter = encodeURIComponent('title eq "' + name + '"')
985
+ const found = await fetchjson(
986
+ 'GET',
987
+ addr + '/v1/vaults/' + vaultid + '/items?filter=' + filter,
988
+ auth(),
989
+ )
990
+
991
+ if (200 !== found.status || !Array.isArray(found.body)) {
992
+ throw new SekretoError('sekreto: onepassword error: ' + found.status + ': finding ' + name)
993
+ }
994
+
995
+ if (0 === found.body.length) {
996
+ return undefined
997
+ }
998
+
999
+ const item = await fetchjson(
1000
+ 'GET',
1001
+ addr + '/v1/vaults/' + vaultid + '/items/' + found.body[0].id,
1002
+ auth(),
1003
+ )
1004
+
1005
+ if (200 !== item.status) {
1006
+ throw new SekretoError('sekreto: onepassword error: ' + item.status + ': reading ' + name)
1007
+ }
1008
+
1009
+ const fields = (item.body && item.body.fields) || []
1010
+
1011
+ for (const field of fields) {
1012
+ if (field && 'PASSWORD' === field.purpose) {
1013
+ return undefined === field.value || null === field.value ? undefined : String(field.value)
1014
+ }
1015
+ }
1016
+ for (const field of fields) {
1017
+ if (field && 'value' === field.label) {
1018
+ return undefined === field.value || null === field.value ? undefined : String(field.value)
1019
+ }
1020
+ }
1021
+
1022
+ return undefined
1023
+ },
1024
+ describe: () => 'onepassword:' + (opts.vault || ''),
1025
+ }
1026
+ }
1027
+
1028
+ /** Doppler.
1029
+ *
1030
+ * The whole config is downloaded once - Doppler's own bulk endpoint -
1031
+ * and answered from memory, like a remote .env: `api.token` is the
1032
+ * `API_TOKEN` entry. A service token is config-scoped, so project and
1033
+ * config are only needed with broader tokens. */
1034
+ export function dopplerprovider(options?: {
1035
+ token?: string
1036
+ project?: string
1037
+ config?: string
1038
+ addr?: string
1039
+ }): Provider {
1040
+ const opts = options || {}
1041
+
1042
+ let values: Record<string, string> | undefined
1043
+
1044
+ const load = async (): Promise<Record<string, string>> => {
1045
+ if (undefined !== values) {
1046
+ return values
1047
+ }
1048
+
1049
+ const addr = (opts.addr || 'https://api.doppler.com').replace(/\/$/, '')
1050
+ checkaddr(addr)
1051
+
1052
+ let url = addr + '/v3/configs/config/secrets/download?format=json'
1053
+ if (opts.project) {
1054
+ url += '&project=' + encodeURIComponent(opts.project)
1055
+ }
1056
+ if (opts.config) {
1057
+ url += '&config=' + encodeURIComponent(opts.config)
1058
+ }
1059
+
1060
+ const res = await fetchjson('GET', url, {
1061
+ authorization: 'Bearer ' + (opts.token || ''),
1062
+ })
1063
+
1064
+ if (200 !== res.status || !res.body || 'object' !== typeof res.body) {
1065
+ throw new SekretoError('sekreto: doppler error: ' + res.status)
1066
+ }
1067
+
1068
+ values = {}
1069
+ for (const [key, value] of Object.entries(res.body)) {
1070
+ if (null !== value && undefined !== value) {
1071
+ values[key] = String(value)
1072
+ }
1073
+ }
1074
+
1075
+ return values
1076
+ }
1077
+
1078
+ return {
1079
+ lookup: async (name: string) => (await load())[envkey(name)],
1080
+ describe: () =>
1081
+ 'doppler' + (opts.project ? ':' + opts.project + '/' + (opts.config || '') : ''),
1082
+ }
1083
+ }
1084
+
1085
+ /** Infisical.
1086
+ *
1087
+ * `api.token` reads the secret keyed `API_TOKEN` (Infisical's own
1088
+ * convention is environment-style keys) at a secret path in one
1089
+ * environment of a project. Auth is a token, or a universal-auth
1090
+ * (machine identity) login with clientid/clientsecret. */
1091
+ export function infisicalprovider(options?: {
1092
+ addr?: string
1093
+ token?: string
1094
+ clientid?: string
1095
+ clientsecret?: string
1096
+ project?: string
1097
+ environment?: string
1098
+ path?: string
1099
+ }): Provider {
1100
+ const opts = options || {}
1101
+
1102
+ // A configured token is kept forever; a universal-auth token carries
1103
+ // expiresIn and is renewed shortly before it runs out.
1104
+ let livetoken: string | undefined
1105
+ let renewat = Infinity
1106
+
1107
+ const login = async (addr: string): Promise<string> => {
1108
+ if (opts.token) {
1109
+ return opts.token
1110
+ }
1111
+
1112
+ if (!opts.clientid || !opts.clientsecret) {
1113
+ throw new SekretoError('sekreto: infisical: no token and no client credentials')
1114
+ }
1115
+
1116
+ const res = await fetchjson(
1117
+ 'POST',
1118
+ addr + '/api/v1/auth/universal-auth/login',
1119
+ { 'content-type': 'application/json' },
1120
+ JSON.stringify({ clientId: opts.clientid, clientSecret: opts.clientsecret }),
1121
+ )
1122
+
1123
+ const got = res.body && res.body.accessToken
1124
+ if (200 !== res.status || !got) {
1125
+ throw new SekretoError('sekreto: infisical login failed: ' + res.status)
1126
+ }
1127
+
1128
+ const expires = Number(res.body.expiresIn)
1129
+ renewat = 0 < expires ? Date.now() + Math.max(expires - 60, 1) * 1000 : Infinity
1130
+
1131
+ return String(got)
1132
+ }
1133
+
1134
+ return {
1135
+ lookup: async (name: string) => {
1136
+ const addr = (opts.addr || 'https://app.infisical.com').replace(/\/$/, '')
1137
+ checkaddr(addr)
1138
+
1139
+ const project = opts.project || ''
1140
+ const environment = opts.environment || ''
1141
+ if ('' === project || '' === environment) {
1142
+ throw new SekretoError('sekreto: infisical: no project/environment')
1143
+ }
1144
+
1145
+ if (undefined === livetoken || Date.now() >= renewat) {
1146
+ livetoken = await login(addr)
1147
+ }
1148
+
1149
+ const url =
1150
+ addr +
1151
+ '/api/v3/secrets/raw/' +
1152
+ envkey(name) +
1153
+ '?workspaceId=' +
1154
+ encodeURIComponent(project) +
1155
+ '&environment=' +
1156
+ encodeURIComponent(environment) +
1157
+ '&secretPath=' +
1158
+ encodeURIComponent(opts.path || '/')
1159
+
1160
+ const res = await fetchjson('GET', url, { authorization: 'Bearer ' + livetoken })
1161
+
1162
+ if (404 === res.status) {
1163
+ return undefined
1164
+ }
1165
+
1166
+ if (200 !== res.status) {
1167
+ throw new SekretoError('sekreto: infisical error: ' + res.status)
1168
+ }
1169
+
1170
+ const value = res.body && res.body.secret && res.body.secret.secretValue
1171
+ return undefined === value || null === value ? undefined : String(value)
1172
+ },
1173
+ describe: () => 'infisical:' + (opts.project || '') + '/' + (opts.environment || ''),
1174
+ }
1175
+ }
1176
+
1177
+ /** Build a provider from its declarative form. */
1178
+ export function makeprovider(spec: ProviderSpec): Provider {
1179
+ switch (spec.kind) {
1180
+ case 'env':
1181
+ return envprovider(spec.prefix)
1182
+ case 'dotenv':
1183
+ return dotenvprovider(spec.file || '.env', spec.prefix)
1184
+ case 'memory':
1185
+ return memoryprovider(spec.values || {}, spec.prefix)
1186
+ case 'file':
1187
+ return fileprovider(spec.dir || '', spec.prefix)
1188
+ case 'hashicorp':
1189
+ return hashicorpprovider(spec.addr || '', spec.token || '', {
1190
+ mount: spec.mount,
1191
+ kv: spec.kv,
1192
+ vaultnamespace: spec.vaultnamespace,
1193
+ auth: spec.auth,
1194
+ })
1195
+ case 'boru':
1196
+ return boruprovider({
1197
+ command: spec.command,
1198
+ namespace: spec.namespace,
1199
+ home: spec.home,
1200
+ addr: spec.addr,
1201
+ token: spec.token,
1202
+ mount: spec.mount,
1203
+ })
1204
+ case 'awssecrets':
1205
+ return awssecretsprovider(spec)
1206
+ case 'awsparams':
1207
+ return awsparamsprovider(spec)
1208
+ case 'gcpsecrets':
1209
+ return gcpsecretsprovider(spec)
1210
+ case 'azuresecrets':
1211
+ return azuresecretsprovider(spec)
1212
+ case 'onepassword':
1213
+ return onepasswordprovider(spec)
1214
+ case 'doppler':
1215
+ return dopplerprovider(spec)
1216
+ case 'infisical':
1217
+ return infisicalprovider(spec)
1218
+ default:
1219
+ throw new SekretoError('sekreto: unknown provider kind: ' + String((spec as any).kind))
1220
+ }
1221
+ }