hanzio 1.1.0 → 1.2.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/README.md +70 -4
- package/dist/api-wrapper/builders.d.ts +13 -2
- package/dist/api-wrapper/errors.d.ts +1 -0
- package/dist/api-wrapper/headers.d.ts +2 -1
- package/dist/api-wrapper/transport.d.ts +8 -5
- package/dist/api-wrapper/types.d.ts +1 -0
- package/dist/cache/index.d.ts +26 -0
- package/dist/secrets/core.d.ts +47 -0
- package/dist/secrets/deadline.d.ts +6 -0
- package/dist/secrets/environment.d.ts +9 -0
- package/dist/secrets/errors.d.ts +11 -0
- package/dist/secrets/index.d.ts +13 -25
- package/dist/secrets/infisical-cli.d.ts +6 -0
- package/dist/secrets/infisical-config.d.ts +17 -0
- package/dist/secrets/infisical-session.d.ts +4 -0
- package/dist/secrets/infisical.d.ts +8 -13
- package/dist/secrets/loaders.d.ts +5 -12
- package/dist/secrets/vite.d.ts +7 -3
- package/dist/secrets/worker-loader.d.ts +9 -0
- package/dist/secrets/worker.d.ts +3 -0
- package/dist/src/api-wrapper/index.js +152 -122
- package/dist/src/index.js +3173 -2713
- package/dist/src/jwt/index.js +2 -2
- package/dist/src/p-queue/index.js +3 -3
- package/dist/src/secrets/index.js +670 -402
- package/dist/src/secrets/infisical.js +451 -168
- package/dist/src/secrets/loaders.js +467 -174
- package/dist/src/secrets/vite.js +33 -11
- package/dist/src/secrets/worker.js +275 -0
- package/dist/src/sitemap/index.js +1 -1
- package/dist/src/zod/index.js +7 -7
- package/dist/typedswitch/index.d.ts +9 -5
- package/package.json +12 -3
- package/src/api-wrapper/builders.ts +68 -43
- package/src/api-wrapper/create-client.ts +82 -62
- package/src/api-wrapper/errors.ts +10 -0
- package/src/api-wrapper/headers.ts +14 -1
- package/src/api-wrapper/redaction.ts +1 -0
- package/src/api-wrapper/transport.ts +34 -38
- package/src/api-wrapper/types.ts +1 -0
- package/src/cache/index.ts +228 -61
- package/src/secrets/core.ts +348 -0
- package/src/secrets/deadline.ts +69 -0
- package/src/secrets/environment.ts +69 -0
- package/src/secrets/errors.ts +29 -0
- package/src/secrets/index.ts +79 -210
- package/src/secrets/infisical-cli.ts +99 -0
- package/src/secrets/infisical-config.ts +70 -0
- package/src/secrets/infisical-session.ts +42 -0
- package/src/secrets/infisical.ts +214 -66
- package/src/secrets/loaders.ts +24 -48
- package/src/secrets/vite.ts +44 -15
- package/src/secrets/worker-loader.ts +56 -0
- package/src/secrets/worker.ts +4 -0
- package/src/typedswitch/index.ts +80 -69
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { SecretLoadError } from './errors'
|
|
2
|
+
|
|
3
|
+
export type SecretRequestOptions = {
|
|
4
|
+
readonly timeoutMs?: number
|
|
5
|
+
readonly signal?: AbortSignal
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** One deadline includes authentication, retries, body reads, and validation. */
|
|
9
|
+
export async function withSecretDeadline<T>(
|
|
10
|
+
options: SecretRequestOptions,
|
|
11
|
+
operation: (signal: AbortSignal) => Promise<T>
|
|
12
|
+
): Promise<T> {
|
|
13
|
+
const timeoutMs = options.timeoutMs ?? 30_000
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
!Number.isFinite(timeoutMs) ||
|
|
17
|
+
timeoutMs <= 0 ||
|
|
18
|
+
timeoutMs > 2_147_483_647
|
|
19
|
+
) {
|
|
20
|
+
throw new SecretLoadError(
|
|
21
|
+
'CONFIGURATION',
|
|
22
|
+
'Secret timeoutMs must be a positive finite timer duration.'
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Normalize caller cancellation so arbitrary abort reasons cannot leak secrets.
|
|
27
|
+
const controller = new AbortController()
|
|
28
|
+
const cancel = () =>
|
|
29
|
+
controller.abort(
|
|
30
|
+
new SecretLoadError('ABORTED', 'Secret loading was cancelled.')
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if (options.signal?.aborted) {
|
|
34
|
+
cancel()
|
|
35
|
+
} else {
|
|
36
|
+
options.signal?.addEventListener('abort', cancel, { once: true })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const timer = setTimeout(
|
|
40
|
+
() =>
|
|
41
|
+
controller.abort(
|
|
42
|
+
new SecretLoadError(
|
|
43
|
+
'TIMEOUT',
|
|
44
|
+
`Secret loading timed out after ${timeoutMs}ms.`
|
|
45
|
+
)
|
|
46
|
+
),
|
|
47
|
+
timeoutMs
|
|
48
|
+
)
|
|
49
|
+
let rejectOnAbort: () => void = () => {}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
53
|
+
rejectOnAbort = () => reject(controller.signal.reason)
|
|
54
|
+
controller.signal.addEventListener('abort', rejectOnAbort, { once: true })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (controller.signal.aborted) {
|
|
58
|
+
throw controller.signal.reason
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Reject promptly even if a custom loader ignores the abort signal.
|
|
62
|
+
return await Promise.race([operation(controller.signal), aborted])
|
|
63
|
+
} finally {
|
|
64
|
+
// Neither completed loads nor failures should retain listeners or timers.
|
|
65
|
+
clearTimeout(timer)
|
|
66
|
+
options.signal?.removeEventListener('abort', cancel)
|
|
67
|
+
controller.signal.removeEventListener('abort', rejectOnAbort)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { SecretLoadError } from './errors'
|
|
2
|
+
|
|
3
|
+
export type SecretEnvironment = 'dev' | 'staging' | 'prod'
|
|
4
|
+
|
|
5
|
+
export type SecretEnvironmentOption =
|
|
6
|
+
| SecretEnvironment
|
|
7
|
+
| { readonly slug: string }
|
|
8
|
+
| (() => SecretEnvironment | { readonly slug: string })
|
|
9
|
+
|
|
10
|
+
export type EnvironmentVariables = Record<string, string | undefined>
|
|
11
|
+
|
|
12
|
+
export function getProcessEnvironment(): EnvironmentVariables | undefined {
|
|
13
|
+
return typeof process === 'undefined' ? undefined : process.env
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveSecretEnvironment(
|
|
17
|
+
option?: SecretEnvironmentOption,
|
|
18
|
+
env?: EnvironmentVariables
|
|
19
|
+
): string {
|
|
20
|
+
// Resolve callbacks once; every reload stays in the same environment.
|
|
21
|
+
const explicit = typeof option === 'function' ? option() : option
|
|
22
|
+
|
|
23
|
+
if (typeof explicit === 'object' && explicit !== null) {
|
|
24
|
+
if (
|
|
25
|
+
typeof explicit.slug !== 'string' ||
|
|
26
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(explicit.slug)
|
|
27
|
+
) {
|
|
28
|
+
throw new SecretLoadError(
|
|
29
|
+
'CONFIGURATION',
|
|
30
|
+
'Custom secret environment requires a valid slug.'
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return explicit.slug
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Explicit configuration is strict. Typos must not silently select dev.
|
|
38
|
+
const selected = explicit ?? env?.SECRETS_ENV
|
|
39
|
+
|
|
40
|
+
if (selected !== undefined) {
|
|
41
|
+
if (selected === 'production' || selected === 'prod') {
|
|
42
|
+
return 'prod'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (selected === 'development' || selected === 'dev') {
|
|
46
|
+
return 'dev'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (selected === 'staging') {
|
|
50
|
+
return 'staging'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw new SecretLoadError(
|
|
54
|
+
'CONFIGURATION',
|
|
55
|
+
'Invalid secret environment. Use dev, staging, prod, or an explicit { slug } option.'
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// NODE_ENV retains the conventional development/test fallback.
|
|
60
|
+
if (env?.NODE_ENV === 'production' || env?.NODE_ENV === 'prod') {
|
|
61
|
+
return 'prod'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (env?.NODE_ENV === 'staging') {
|
|
65
|
+
return 'staging'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return 'dev'
|
|
69
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type SecretErrorCode =
|
|
2
|
+
| 'CONFIGURATION'
|
|
3
|
+
| 'AUTHENTICATION'
|
|
4
|
+
| 'HTTP'
|
|
5
|
+
| 'CLI'
|
|
6
|
+
| 'VALIDATION'
|
|
7
|
+
| 'MISSING_SECRETS'
|
|
8
|
+
| 'TIMEOUT'
|
|
9
|
+
| 'ABORTED'
|
|
10
|
+
| 'LOADER'
|
|
11
|
+
|
|
12
|
+
/** Contains only safe diagnostic metadata; never attach underlying errors. */
|
|
13
|
+
export class SecretLoadError extends Error {
|
|
14
|
+
readonly code: SecretErrorCode
|
|
15
|
+
readonly operation?: string
|
|
16
|
+
readonly status?: number
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
code: SecretErrorCode,
|
|
20
|
+
message: string,
|
|
21
|
+
details: { operation?: string; status?: number } = {}
|
|
22
|
+
) {
|
|
23
|
+
super(message)
|
|
24
|
+
this.name = 'SecretLoadError'
|
|
25
|
+
this.code = code
|
|
26
|
+
this.operation = details.operation
|
|
27
|
+
this.status = details.status
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/secrets/index.ts
CHANGED
|
@@ -1,225 +1,94 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export type DefineSecretSetOptions<SecretKey extends string> =
|
|
25
|
-
| InfisicalDefineSecretSetOptions
|
|
26
|
-
| CustomLoaderDefineSecretSetOptions<SecretKey>
|
|
27
|
-
|
|
28
|
-
export type SecretSetLoaderContext<SecretKey extends string> = {
|
|
29
|
-
readonly keys: readonly SecretKey[]
|
|
30
|
-
readonly environment: SecretEnvironment
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type SecretSetLoader<SecretKey extends string> = (
|
|
34
|
-
context: SecretSetLoaderContext<SecretKey>
|
|
35
|
-
) => Promise<Partial<Record<SecretKey, string>>>
|
|
36
|
-
|
|
37
|
-
type ResolvedSecretSetOptions<SecretKey extends string> = {
|
|
38
|
-
readonly loader: SecretSetLoader<SecretKey>
|
|
39
|
-
readonly environment: SecretEnvironment
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type SecretSet<SecretKey extends string> = {
|
|
43
|
-
secret: (key: SecretKey) => string
|
|
44
|
-
secrets: () => Record<SecretKey, string>
|
|
45
|
-
reload: () => Promise<SecretSet<SecretKey>>
|
|
46
|
-
}
|
|
47
|
-
|
|
1
|
+
import {
|
|
2
|
+
defineSecretSet as defineCoreSecretSet,
|
|
3
|
+
type CommonSecretSetOptions,
|
|
4
|
+
type CustomLoaderDefineSecretSetOptions,
|
|
5
|
+
type SecretSet,
|
|
6
|
+
type SecretSetLoader
|
|
7
|
+
} from './core'
|
|
8
|
+
import {
|
|
9
|
+
getProcessEnvironment,
|
|
10
|
+
resolveSecretEnvironment,
|
|
11
|
+
type SecretEnvironment
|
|
12
|
+
} from './environment'
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_INFISICAL_SITE_URL,
|
|
15
|
+
loadInfisicalSecrets,
|
|
16
|
+
type InfisicalAuthMode
|
|
17
|
+
} from './infisical'
|
|
18
|
+
import {
|
|
19
|
+
validateInfisicalOrganizationId,
|
|
20
|
+
type InfisicalProjectOptions
|
|
21
|
+
} from './infisical-config'
|
|
22
|
+
|
|
23
|
+
export * from './core'
|
|
48
24
|
export * from './infisical'
|
|
49
25
|
export * from './loaders'
|
|
50
26
|
export * from './vite'
|
|
27
|
+
export type { SecretRequestOptions } from './deadline'
|
|
28
|
+
|
|
29
|
+
export type InfisicalDefineSecretSetOptions<
|
|
30
|
+
K extends string = string,
|
|
31
|
+
Parsed = Record<K, string>
|
|
32
|
+
> = CommonSecretSetOptions<K, Parsed> &
|
|
33
|
+
InfisicalProjectOptions & {
|
|
34
|
+
readonly auth?: InfisicalAuthMode
|
|
35
|
+
readonly clientIdEnvKey?: string
|
|
36
|
+
readonly clientSecretEnvKey?: string
|
|
37
|
+
readonly loader?: undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type DefineSecretSetOptions<
|
|
41
|
+
K extends string,
|
|
42
|
+
Parsed = Record<K, string>
|
|
43
|
+
> =
|
|
44
|
+
| InfisicalDefineSecretSetOptions<K, Parsed>
|
|
45
|
+
| CustomLoaderDefineSecretSetOptions<K, Parsed>
|
|
51
46
|
|
|
52
47
|
export function getSecretEnvironment(): SecretEnvironment {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
return resolveSecretEnvironment(
|
|
49
|
+
undefined,
|
|
50
|
+
getProcessEnvironment()
|
|
51
|
+
) as SecretEnvironment
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
export async function defineSecretSet<
|
|
60
|
-
const
|
|
55
|
+
const Keys extends readonly string[],
|
|
56
|
+
Parsed = Record<Keys[number], string>
|
|
61
57
|
>(
|
|
62
|
-
keys:
|
|
63
|
-
options: DefineSecretSetOptions<
|
|
64
|
-
): Promise<SecretSet<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
keySet,
|
|
74
|
-
cachedSecrets,
|
|
75
|
-
force,
|
|
76
|
-
...resolvedOptions
|
|
77
|
-
})
|
|
78
|
-
cachedSecrets = nextSecrets
|
|
79
|
-
return nextSecrets
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const secretSet = createSecretSet({
|
|
83
|
-
getCachedSecrets: () => cachedSecrets,
|
|
84
|
-
reload: loadSecrets
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
await loadSecrets()
|
|
88
|
-
return secretSet
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function resolveSecretSetOptions<SecretKey extends string>(
|
|
92
|
-
options: DefineSecretSetOptions<SecretKey>
|
|
93
|
-
): ResolvedSecretSetOptions<SecretKey> {
|
|
94
|
-
const environment = resolveEnvironment(options.environment)
|
|
95
|
-
const loader: SecretSetLoader<SecretKey> = options.loader
|
|
96
|
-
? options.loader
|
|
97
|
-
: (context) =>
|
|
98
|
-
loadInfisicalSecrets({
|
|
99
|
-
...context,
|
|
100
|
-
projectId: options.projectId,
|
|
101
|
-
siteUrl: options.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
|
|
102
|
-
clientIdEnvKey: options.clientIdEnvKey ?? 'INFISICAL_CLIENT_ID',
|
|
103
|
-
clientSecretEnvKey:
|
|
104
|
-
options.clientSecretEnvKey ?? 'INFISICAL_CLIENT_SECRET'
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
loader,
|
|
109
|
-
environment
|
|
58
|
+
keys: Keys,
|
|
59
|
+
options: DefineSecretSetOptions<Keys[number], Parsed>
|
|
60
|
+
): Promise<SecretSet<Keys[number], Parsed>> {
|
|
61
|
+
let loader: SecretSetLoader<Keys[number]>
|
|
62
|
+
|
|
63
|
+
if (!options.loader) {
|
|
64
|
+
// Validate even when environment overrides satisfy every key.
|
|
65
|
+
validateInfisicalOrganizationId(options.organizationId)
|
|
66
|
+
loader = createDefaultInfisicalLoader(options)
|
|
67
|
+
} else {
|
|
68
|
+
loader = options.loader
|
|
110
69
|
}
|
|
111
|
-
}
|
|
112
70
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
force
|
|
119
|
-
}: {
|
|
120
|
-
readonly keySet: readonly SecretKey[]
|
|
121
|
-
readonly cachedSecrets: Record<SecretKey, string> | null
|
|
122
|
-
readonly loader: SecretSetLoader<SecretKey>
|
|
123
|
-
readonly environment: SecretEnvironment
|
|
124
|
-
readonly force: boolean
|
|
125
|
-
}): Promise<Record<SecretKey, string>> {
|
|
126
|
-
const processEnvSecrets = readProcessEnvSecrets(
|
|
127
|
-
keySet,
|
|
128
|
-
force ? cachedSecrets : null
|
|
71
|
+
// Supply process.env only from this Node/Bun entry point, keeping the core portable.
|
|
72
|
+
return defineCoreSecretSet(
|
|
73
|
+
keys,
|
|
74
|
+
{ ...options, loader },
|
|
75
|
+
getProcessEnvironment()
|
|
129
76
|
)
|
|
130
|
-
const keysMissingFromEnv = keySet.filter((key) => !processEnvSecrets[key])
|
|
131
|
-
|
|
132
|
-
logSecretEnvironment(environment, keySet.length)
|
|
133
|
-
|
|
134
|
-
const loaderKeys = force ? keySet : keysMissingFromEnv
|
|
135
|
-
const loadedSecrets: Partial<Record<SecretKey, string>> =
|
|
136
|
-
loaderKeys.length === 0
|
|
137
|
-
? {}
|
|
138
|
-
: await loader({
|
|
139
|
-
keys: loaderKeys,
|
|
140
|
-
environment
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
const missingKeys = keySet.filter(
|
|
144
|
-
(key) => !(processEnvSecrets[key] ?? loadedSecrets[key])
|
|
145
|
-
)
|
|
146
|
-
if (missingKeys.length > 0) {
|
|
147
|
-
throw new Error(`Missing secrets: ${missingKeys.join(', ')}`)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const nextSecrets = Object.fromEntries(
|
|
151
|
-
keySet.map((key) => [
|
|
152
|
-
key,
|
|
153
|
-
(processEnvSecrets[key] ?? loadedSecrets[key]) as string
|
|
154
|
-
])
|
|
155
|
-
) as Record<SecretKey, string>
|
|
156
|
-
|
|
157
|
-
for (const [key, value] of Object.entries(nextSecrets)) {
|
|
158
|
-
process.env[key] = value as string
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return nextSecrets
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function createSecretSet<SecretKey extends string>({
|
|
165
|
-
getCachedSecrets,
|
|
166
|
-
reload
|
|
167
|
-
}: {
|
|
168
|
-
readonly getCachedSecrets: () => Record<SecretKey, string> | null
|
|
169
|
-
readonly reload: (force?: boolean) => Promise<Record<SecretKey, string>>
|
|
170
|
-
}): SecretSet<SecretKey> {
|
|
171
|
-
const secretSet: SecretSet<SecretKey> = {
|
|
172
|
-
secret(key) {
|
|
173
|
-
const value = getCachedSecrets()?.[key] ?? process.env[key]
|
|
174
|
-
if (!value) {
|
|
175
|
-
throw new Error(`Secret ${key} not found`)
|
|
176
|
-
}
|
|
177
|
-
return value
|
|
178
|
-
},
|
|
179
|
-
secrets() {
|
|
180
|
-
const cachedSecrets = getCachedSecrets()
|
|
181
|
-
if (!cachedSecrets) {
|
|
182
|
-
throw new Error('Secrets not initialized.')
|
|
183
|
-
}
|
|
184
|
-
return { ...cachedSecrets }
|
|
185
|
-
},
|
|
186
|
-
async reload() {
|
|
187
|
-
await reload(true)
|
|
188
|
-
return secretSet
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return secretSet
|
|
193
77
|
}
|
|
194
78
|
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
):
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function readProcessEnvSecrets<SecretKey extends string>(
|
|
213
|
-
keys: readonly SecretKey[],
|
|
214
|
-
previousSecrets: Record<SecretKey, string> | null = null
|
|
215
|
-
): Partial<Record<SecretKey, string>> {
|
|
216
|
-
// On forced reloads, skip cached values so the loader can refresh them.
|
|
217
|
-
return Object.fromEntries(
|
|
218
|
-
keys
|
|
219
|
-
.filter((key) => {
|
|
220
|
-
const value = process.env[key]
|
|
221
|
-
return value && value !== previousSecrets?.[key]
|
|
222
|
-
})
|
|
223
|
-
.map((key) => [key, process.env[key] as string])
|
|
224
|
-
) as Partial<Record<SecretKey, string>>
|
|
79
|
+
function createDefaultInfisicalLoader<K extends string, Parsed>(
|
|
80
|
+
options: InfisicalDefineSecretSetOptions<K, Parsed>
|
|
81
|
+
): SecretSetLoader<K> {
|
|
82
|
+
return (context) =>
|
|
83
|
+
loadInfisicalSecrets({
|
|
84
|
+
...context,
|
|
85
|
+
projectId: options.projectId,
|
|
86
|
+
organizationId: options.organizationId,
|
|
87
|
+
siteUrl: options.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
|
|
88
|
+
secretPath: options.secretPath,
|
|
89
|
+
auth: options.auth,
|
|
90
|
+
clientIdEnvKey: options.clientIdEnvKey ?? 'INFISICAL_CLIENT_ID',
|
|
91
|
+
clientSecretEnvKey:
|
|
92
|
+
options.clientSecretEnvKey ?? 'INFISICAL_CLIENT_SECRET'
|
|
93
|
+
})
|
|
225
94
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { SecretRequestOptions } from './deadline'
|
|
2
|
+
import { getProcessEnvironment } from './environment'
|
|
3
|
+
import { SecretLoadError } from './errors'
|
|
4
|
+
|
|
5
|
+
/** Read the active CLI session without a shell or credential-bearing errors. */
|
|
6
|
+
export async function readInfisicalCliToken(
|
|
7
|
+
options: SecretRequestOptions & { siteUrl: string; signal: AbortSignal }
|
|
8
|
+
): Promise<string> {
|
|
9
|
+
// Import subprocess support only when the CLI is actually selected.
|
|
10
|
+
const { execFile } = await import('node:child_process').catch(() => {
|
|
11
|
+
throw new SecretLoadError(
|
|
12
|
+
'CLI',
|
|
13
|
+
'Infisical CLI fallback requires a runtime with subprocess support. Provide Infisical client credentials or a custom secret loader.'
|
|
14
|
+
)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
options.signal.throwIfAborted()
|
|
18
|
+
|
|
19
|
+
const env: Record<string, string | undefined> = {
|
|
20
|
+
...getProcessEnvironment(),
|
|
21
|
+
INFISICAL_DISABLE_UPDATE_CHECK: 'true'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Inherited machine tokens would override the CLI's signed-in user session.
|
|
25
|
+
// Remove them from the child copy without changing the parent's environment.
|
|
26
|
+
delete env.INFISICAL_TOKEN
|
|
27
|
+
delete env.INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN
|
|
28
|
+
delete env.TOKEN
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const stdout = await new Promise<string>((resolve, reject) => {
|
|
32
|
+
const child = execFile(
|
|
33
|
+
'infisical',
|
|
34
|
+
[
|
|
35
|
+
'user',
|
|
36
|
+
'get',
|
|
37
|
+
'token',
|
|
38
|
+
'--plain',
|
|
39
|
+
`--domain=${options.siteUrl}`,
|
|
40
|
+
'--silent'
|
|
41
|
+
],
|
|
42
|
+
{
|
|
43
|
+
env,
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
shell: false,
|
|
46
|
+
timeout: options.timeoutMs ?? 30_000,
|
|
47
|
+
signal: options.signal,
|
|
48
|
+
killSignal: 'SIGKILL',
|
|
49
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
50
|
+
windowsHide: true
|
|
51
|
+
},
|
|
52
|
+
(error, output) => {
|
|
53
|
+
if (error) {
|
|
54
|
+
reject(error)
|
|
55
|
+
} else {
|
|
56
|
+
resolve(output)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// Startup cannot wait for interactive input.
|
|
62
|
+
child.stdin?.end()
|
|
63
|
+
})
|
|
64
|
+
return stdout.trim()
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (options.signal.aborted) {
|
|
67
|
+
throw options.signal.reason
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Subprocess errors can contain secret stdout/stderr, so rebuild the error.
|
|
71
|
+
const failure = error as { code?: string | number; killed?: boolean } | null
|
|
72
|
+
|
|
73
|
+
if (failure?.code === 'ENOENT') {
|
|
74
|
+
throw new SecretLoadError(
|
|
75
|
+
'CLI',
|
|
76
|
+
'Infisical CLI not found. Install infisical and ensure it is on PATH, or provide Infisical client credentials.'
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (failure?.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
81
|
+
throw new SecretLoadError(
|
|
82
|
+
'CLI',
|
|
83
|
+
'Infisical CLI output exceeded the 10 MiB limit.'
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (failure?.killed) {
|
|
88
|
+
throw new SecretLoadError(
|
|
89
|
+
'TIMEOUT',
|
|
90
|
+
`Infisical CLI timed out after ${(options.timeoutMs ?? 30_000) / 1000} seconds or was terminated.`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw new SecretLoadError(
|
|
95
|
+
'CLI',
|
|
96
|
+
`Infisical CLI session token failed. Run infisical login --domain=${options.siteUrl} and verify access to the configured organization, project, and environment.`
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { SecretRequestOptions } from './deadline'
|
|
2
|
+
import { SecretLoadError } from './errors'
|
|
3
|
+
|
|
4
|
+
/** Project scope shared by the public Infisical loaders. */
|
|
5
|
+
export type InfisicalProjectOptions = {
|
|
6
|
+
readonly projectId: string
|
|
7
|
+
|
|
8
|
+
/** Scope CLI user authentication; machine identities already have an organization. */
|
|
9
|
+
readonly organizationId: string
|
|
10
|
+
readonly siteUrl?: string
|
|
11
|
+
readonly secretPath?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type InfisicalScope<K extends string> = InfisicalProjectOptions &
|
|
15
|
+
SecretRequestOptions & {
|
|
16
|
+
readonly keys: readonly K[]
|
|
17
|
+
readonly environment: string
|
|
18
|
+
readonly siteUrl: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateInfisicalScope(options: InfisicalScope<string>): void {
|
|
22
|
+
validateInfisicalOrganizationId(options.organizationId)
|
|
23
|
+
|
|
24
|
+
let url: URL
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
url = new URL(options.siteUrl)
|
|
28
|
+
} catch {
|
|
29
|
+
throw new SecretLoadError('CONFIGURATION', 'Invalid Infisical siteUrl.')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Keep credentials and other sensitive URL components out of diagnostics.
|
|
33
|
+
if (
|
|
34
|
+
!['http:', 'https:'].includes(url.protocol) ||
|
|
35
|
+
url.username ||
|
|
36
|
+
url.password ||
|
|
37
|
+
url.search ||
|
|
38
|
+
url.hash
|
|
39
|
+
) {
|
|
40
|
+
throw new SecretLoadError(
|
|
41
|
+
'CONFIGURATION',
|
|
42
|
+
'Infisical siteUrl must be an HTTP(S) URL without credentials, query, or fragment.'
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
!options.projectId.trim() ||
|
|
48
|
+
!options.environment.trim() ||
|
|
49
|
+
!(options.secretPath ?? '/').startsWith('/')
|
|
50
|
+
) {
|
|
51
|
+
throw new SecretLoadError(
|
|
52
|
+
'CONFIGURATION',
|
|
53
|
+
'Infisical requires a projectId, environment, and absolute secretPath.'
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Validate at configuration time, including for JavaScript callers and local-only loads. */
|
|
59
|
+
export function validateInfisicalOrganizationId(organizationId: unknown): void {
|
|
60
|
+
if (
|
|
61
|
+
typeof organizationId !== 'string' ||
|
|
62
|
+
!organizationId.trim() ||
|
|
63
|
+
/\s/.test(organizationId)
|
|
64
|
+
) {
|
|
65
|
+
throw new SecretLoadError(
|
|
66
|
+
'CONFIGURATION',
|
|
67
|
+
'Infisical organizationId is required and must be a nonempty ID without whitespace.'
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { SecretLoadError } from './errors'
|
|
3
|
+
|
|
4
|
+
const sessionClaimsSchema = z.object({
|
|
5
|
+
organizationId: z.string().optional(),
|
|
6
|
+
subOrganizationId: z.string().optional(),
|
|
7
|
+
exp: z.number().finite().optional()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
/** Read routing metadata only. JWT signature and permissions are verified by Infisical. */
|
|
11
|
+
export function parseInfisicalSessionToken(token: string): {
|
|
12
|
+
organizationId?: string
|
|
13
|
+
} {
|
|
14
|
+
let claims: z.infer<typeof sessionClaimsSchema>
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Reject empty, formatted, or multiline CLI output before making an HTTP request.
|
|
18
|
+
if (!/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token)) {
|
|
19
|
+
throw new Error('Invalid token format')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const payload = token.split('.')[1]!
|
|
23
|
+
const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
|
|
24
|
+
|
|
25
|
+
claims = sessionClaimsSchema.parse(JSON.parse(json))
|
|
26
|
+
} catch {
|
|
27
|
+
throw new SecretLoadError(
|
|
28
|
+
'AUTHENTICATION',
|
|
29
|
+
'Infisical returned an invalid user session token. Run infisical login for the configured domain.'
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (claims.exp !== undefined && claims.exp <= Date.now() / 1000) {
|
|
34
|
+
throw new SecretLoadError(
|
|
35
|
+
'AUTHENTICATION',
|
|
36
|
+
'Infisical user session has expired. Run infisical login for the configured domain.'
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// For sub-organizations, the root organization claim does not identify the scope.
|
|
41
|
+
return { organizationId: claims.subOrganizationId ?? claims.organizationId }
|
|
42
|
+
}
|