@voxgig/sdkgen 4.6.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/cmp/Feature.js +5 -0
- package/dist/cmp/Feature.js.map +1 -1
- package/dist/cmp/TestControl.d.ts +4 -0
- package/dist/cmp/TestControl.js +51 -0
- package/dist/cmp/TestControl.js.map +1 -0
- package/dist/helpers/featureSource.d.ts +3 -1
- package/dist/helpers/featureSource.js +105 -0
- package/dist/helpers/featureSource.js.map +1 -1
- package/dist/sdkgen.d.ts +3 -2
- package/dist/sdkgen.js +9 -4
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +52 -0
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.aon +58 -0
- package/package.json +1 -1
- package/project/.sdk/model/feature/secrets.aon +82 -0
- package/project/.sdk/src/cmp/cpp/Main_cpp.ts +2 -1
- package/project/.sdk/src/cmp/cpp/Test_cpp.ts +5 -1
- package/project/.sdk/src/cmp/csharp/Main_csharp.ts +2 -1
- package/project/.sdk/src/cmp/csharp/TestDirect_csharp.ts +30 -6
- package/project/.sdk/src/cmp/csharp/TestEntity_csharp.ts +29 -5
- package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -1
- package/project/.sdk/src/cmp/dart/Main_dart.ts +2 -1
- package/project/.sdk/src/cmp/dart/TestDirect_dart.ts +23 -6
- package/project/.sdk/src/cmp/dart/TestEntity_dart.ts +28 -7
- package/project/.sdk/src/cmp/dart/Test_dart.ts +5 -1
- package/project/.sdk/src/cmp/go/Main_go.ts +2 -1
- package/project/.sdk/src/cmp/go/Test_go.ts +5 -1
- package/project/.sdk/src/cmp/java/Main_java.ts +2 -1
- package/project/.sdk/src/cmp/java/Test_java.ts +5 -1
- package/project/.sdk/src/cmp/js/Main_js.ts +2 -1
- package/project/.sdk/src/cmp/js/TestDirect_js.ts +30 -5
- package/project/.sdk/src/cmp/js/TestEntity_js.ts +35 -5
- package/project/.sdk/src/cmp/js/Test_js.ts +5 -1
- package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +7 -1
- package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +7 -1
- package/project/.sdk/src/cmp/kotlin/Main_kotlin.ts +2 -1
- package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +5 -1
- package/project/.sdk/src/cmp/lua/Main_lua.ts +2 -1
- package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +28 -4
- package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +24 -4
- package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -1
- package/project/.sdk/src/cmp/ocaml/Gitignore_ocaml.ts +1 -0
- package/project/.sdk/src/cmp/perl/Main_perl.ts +2 -1
- package/project/.sdk/src/cmp/perl/TestDirect_perl.ts +25 -4
- package/project/.sdk/src/cmp/perl/TestEntity_perl.ts +24 -4
- package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -1
- package/project/.sdk/src/cmp/perl/utility_perl.ts +10 -0
- package/project/.sdk/src/cmp/php/Main_php.ts +2 -1
- package/project/.sdk/src/cmp/php/TestDirect_php.ts +24 -5
- package/project/.sdk/src/cmp/php/TestEntity_php.ts +28 -4
- package/project/.sdk/src/cmp/php/Test_php.ts +5 -1
- package/project/.sdk/src/cmp/py/Main_py.ts +2 -1
- package/project/.sdk/src/cmp/py/Test_py.ts +5 -1
- package/project/.sdk/src/cmp/rb/Main_rb.ts +2 -1
- package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +24 -5
- package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +24 -4
- package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -1
- package/project/.sdk/src/cmp/rb/utility_rb.ts +3 -1
- package/project/.sdk/src/cmp/rust/Main_rust.ts +2 -1
- package/project/.sdk/src/cmp/rust/TestDirect_rust.ts +31 -4
- package/project/.sdk/src/cmp/rust/TestEntity_rust.ts +37 -4
- package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -1
- package/project/.sdk/src/cmp/scala/Main_scala.ts +2 -1
- package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -1
- package/project/.sdk/src/cmp/ts/Config_ts.ts +50 -8
- package/project/.sdk/src/cmp/ts/Main_ts.ts +10 -2
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +10 -3
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +11 -3
- package/project/.sdk/src/cmp/ts/Test_ts.ts +5 -1
- package/project/.sdk/tm/csharp/test/Runner.cs +45 -0
- package/project/.sdk/tm/dart/test/utility.dart +39 -0
- package/project/.sdk/tm/js/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/js/test/utility.js +91 -1
- package/project/.sdk/tm/lua/test/runner.lua +40 -0
- package/project/.sdk/tm/ocaml/test/harness.ml +14 -4
- package/project/.sdk/tm/perl/t/runner.pm +30 -0
- package/project/.sdk/tm/php/test/Runner.php +36 -0
- package/project/.sdk/tm/rb/test/runner.rb +25 -0
- package/project/.sdk/tm/rust/tests/common/mod.rs +32 -0
- package/project/.sdk/tm/rust/utility/make_options.rs +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +8 -2
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +29 -5
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +30 -21
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/Registry.ts +90 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/addr.ts +48 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/aws.ts +219 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/azuresecrets.ts +144 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/boru.ts +113 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/doppler.ts +77 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/dotenv.ts +51 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/env.ts +31 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/file.ts +54 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/gcpsecrets.ts +118 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/hashicorp.ts +159 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/http.ts +66 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/infisical.ts +107 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/memory.ts +33 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/onepassword.ts +120 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/support.ts +176 -0
- package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +15 -13
- package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +25 -19
- package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +1 -1
- package/project/.sdk/tm/ts/test/vendor/omni/index.ts +1 -1
- package/project/.sdk/tm/zig/core/utility.zig +1 -1
- package/project/sdkgen-package.json +1 -1
- package/src/cmp/Feature.ts +5 -0
- package/src/cmp/TestControl.ts +64 -0
- package/src/helpers/featureSource.ts +130 -0
- package/src/sdkgen.ts +7 -0
- package/src/utility.ts +61 -0
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Providers.ts +0 -1221
|
@@ -1,1221 +0,0 @@
|
|
|
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
|
-
}
|