hanzio 1.1.0 → 2.0.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 +832 -219
- package/dist/api-wrapper/builders.d.ts +15 -3
- package/dist/api-wrapper/create-client.d.ts +1 -1
- package/dist/api-wrapper/describe.d.ts +16 -0
- package/dist/api-wrapper/endpoint.d.ts +18 -0
- package/dist/api-wrapper/errors.d.ts +40 -2
- package/dist/api-wrapper/headers.d.ts +2 -1
- package/dist/api-wrapper/index.d.ts +5 -0
- package/dist/api-wrapper/otel.d.ts +44 -0
- package/dist/api-wrapper/paginate.d.ts +12 -0
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -2
- package/dist/api-wrapper/testing.d.ts +23 -0
- package/dist/api-wrapper/transport.d.ts +12 -10
- package/dist/api-wrapper/types.d.ts +104 -18
- package/dist/array/index.d.ts +50 -4
- package/dist/cache/createCache.d.ts +31 -0
- package/dist/cache/index.d.ts +45 -2
- package/dist/color/contrast.d.ts +40 -0
- package/dist/color/index.d.ts +2 -0
- package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
- package/dist/cool-console-log/index.d.ts +34 -5
- package/dist/crypto/encoding.d.ts +18 -0
- package/dist/crypto/hash.d.ts +33 -0
- package/dist/crypto/index.d.ts +4 -0
- package/dist/crypto/sealer.d.ts +25 -0
- package/dist/crypto/webhook.d.ts +20 -0
- package/dist/date/index.d.ts +46 -2
- package/dist/emitter/index.d.ts +21 -0
- package/dist/error/index.d.ts +12 -0
- package/dist/format/index.d.ts +102 -0
- package/dist/function/index.d.ts +60 -0
- package/dist/guard/index.d.ts +20 -0
- package/dist/i18n/index.d.ts +37 -0
- package/dist/index-0zvtcbzy.js +224 -0
- package/dist/index-181e6vfw.js +29 -0
- package/dist/index-8r2vdzjg.js +87 -0
- package/dist/index-hy7kbb99.js +157 -0
- package/dist/{src/secrets/loaders.js → index-j1ybktbr.js} +608 -405
- package/dist/index-k9yx6bne.js +89 -0
- package/dist/index-kj1phgrx.js +169 -0
- package/dist/index-mnkcfyx8.js +78 -0
- package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
- package/dist/index-rp5pqghz.js +157 -0
- package/dist/index-srt06xd2.js +15 -0
- package/dist/index-wjyb9bsd.js +263 -0
- package/dist/index.d.ts +7 -7
- package/dist/jwt/index.d.ts +50 -2
- package/dist/math/index.d.ts +2 -2
- package/dist/math/linearRegressionTrend.d.ts +14 -1
- package/dist/math/scale.d.ts +8 -0
- package/dist/object/index.d.ts +19 -0
- package/dist/p-queue/index.d.ts +1 -1
- package/dist/promise/index.d.ts +97 -2
- 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/sitemap/index.d.ts +48 -6
- package/dist/src/api-wrapper/index.js +60 -1100
- package/dist/src/api-wrapper/testing.js +20 -0
- package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
- package/dist/src/crypto/index.js +195 -0
- package/dist/src/format/index.js +211 -0
- package/dist/src/i18n/index.js +36 -0
- package/dist/src/index.js +863 -2809
- package/dist/src/jwt/index.js +138 -92
- package/dist/src/p-queue/index.js +9 -437
- package/dist/src/secrets/index.js +261 -1377
- package/dist/src/secrets/vite.js +6 -17
- package/dist/src/secrets/worker.js +12 -0
- package/dist/src/sitemap/index.js +183 -511
- package/dist/src/storage/index.js +119 -0
- package/dist/src/zod/index.js +347 -174
- package/dist/state/index.d.ts +21 -0
- package/dist/storage/index.d.ts +58 -0
- package/dist/string/index.d.ts +75 -2
- package/dist/typedswitch/index.d.ts +37 -26
- package/dist/types/index.d.ts +11 -0
- package/dist/url/index.d.ts +50 -1
- package/dist/zod/index.d.ts +32 -3
- package/dist/zod/introspect.d.ts +19 -0
- package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
- package/dist/zod/zodMinMaxFilter.d.ts +22 -3
- package/dist/zod/zodToTypeString.d.ts +8 -3
- package/package.json +87 -7
- package/src/api-wrapper/actions-impl.ts +11 -72
- package/src/api-wrapper/builders.ts +84 -52
- package/src/api-wrapper/create-client.ts +246 -96
- package/src/api-wrapper/describe.ts +157 -0
- package/src/api-wrapper/endpoint.ts +26 -0
- package/src/api-wrapper/error-reporting.ts +7 -1
- package/src/api-wrapper/errors.ts +117 -2
- package/src/api-wrapper/headers.ts +19 -1
- package/src/api-wrapper/index.ts +5 -0
- package/src/api-wrapper/otel.ts +139 -0
- package/src/api-wrapper/paginate.ts +28 -0
- package/src/api-wrapper/redaction.ts +43 -0
- package/src/api-wrapper/shared.ts +21 -2
- package/src/api-wrapper/testing.ts +48 -0
- package/src/api-wrapper/transport.ts +68 -74
- package/src/api-wrapper/types.ts +186 -18
- package/src/array/index.ts +195 -42
- package/src/cache/createCache.ts +157 -0
- package/src/cache/index.ts +235 -83
- package/src/color/contrast.ts +163 -0
- package/src/color/index.ts +22 -0
- package/src/{math → color}/metricsSeriesColors.ts +47 -49
- package/src/cool-console-log/index.ts +128 -41
- package/src/crypto/encoding.ts +83 -0
- package/src/crypto/hash.ts +170 -0
- package/src/crypto/index.ts +26 -0
- package/src/crypto/sealer.ts +168 -0
- package/src/crypto/webhook.ts +86 -0
- package/src/date/index.ts +109 -34
- package/src/emitter/index.ts +82 -0
- package/src/error/index.ts +63 -0
- package/src/format/index.ts +389 -0
- package/src/function/index.ts +201 -0
- package/src/guard/index.ts +80 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +11 -7
- package/src/jwt/index.ts +199 -125
- package/src/math/index.ts +5 -10
- package/src/math/linearRegressionTrend.ts +33 -8
- package/src/math/scale.ts +17 -0
- package/src/object/index.ts +99 -0
- package/src/p-queue/index.ts +2 -0
- package/src/promise/index.ts +304 -9
- 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/sitemap/index.ts +293 -130
- package/src/state/index.ts +133 -81
- package/src/storage/index.ts +187 -0
- package/src/string/index.ts +261 -23
- package/src/typedswitch/index.ts +134 -103
- package/src/types/index.ts +14 -0
- package/src/url/index.ts +146 -1
- package/src/zod/index.ts +105 -9
- package/src/zod/introspect.ts +103 -0
- package/src/zod/jsonToZodSchemaMapper.ts +44 -21
- package/src/zod/zodMinMaxFilter.ts +37 -13
- package/src/zod/zodToTypeString.ts +184 -181
- package/dist/api-wrapper/index.js +0 -250
- package/dist/gzip/index.d.ts +0 -2
- package/dist/gzip/index.js +0 -16
- package/dist/index.js +0 -1823
- package/dist/jwt/index.js +0 -19
- package/dist/sitemap/index.js +0 -552
- package/dist/src/gzip/index.js +0 -16
- package/dist/src/secrets/infisical.js +0 -1194
- package/dist/zod/index.js +0 -210
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { withSecretDeadline, type SecretRequestOptions } from './deadline'
|
|
2
|
+
import {
|
|
3
|
+
resolveSecretEnvironment,
|
|
4
|
+
type EnvironmentVariables,
|
|
5
|
+
type SecretEnvironmentOption
|
|
6
|
+
} from './environment'
|
|
7
|
+
import { SecretLoadError } from './errors'
|
|
8
|
+
|
|
9
|
+
export type { SecretEnvironment, SecretEnvironmentOption } from './environment'
|
|
10
|
+
export { SecretLoadError } from './errors'
|
|
11
|
+
|
|
12
|
+
export type SecretLogAttributes = {
|
|
13
|
+
readonly environment: string
|
|
14
|
+
readonly count: number
|
|
15
|
+
readonly durationMs: number
|
|
16
|
+
readonly source?: string
|
|
17
|
+
readonly sources?: Readonly<Record<string, number>>
|
|
18
|
+
readonly code?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SecretLogger = {
|
|
22
|
+
info: (message: string, attributes: SecretLogAttributes) => void
|
|
23
|
+
error: (message: string, attributes: SecretLogAttributes) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SecretSchema<K extends string, Parsed> = {
|
|
27
|
+
parseAsync: (values: Record<K, string>) => Promise<Parsed>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type CommonSecretSetOptions<
|
|
31
|
+
K extends string,
|
|
32
|
+
Parsed = Record<K, string>
|
|
33
|
+
> = SecretRequestOptions & {
|
|
34
|
+
readonly environment?: SecretEnvironmentOption
|
|
35
|
+
readonly writeToProcessEnv?: boolean
|
|
36
|
+
readonly logger?: SecretLogger | false
|
|
37
|
+
readonly schema?: SecretSchema<K, Parsed>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type CustomLoaderDefineSecretSetOptions<
|
|
41
|
+
K extends string,
|
|
42
|
+
Parsed = Record<K, string>
|
|
43
|
+
> = CommonSecretSetOptions<K, Parsed> & {
|
|
44
|
+
readonly loader: SecretSetLoader<K>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type SecretSetLoaderContext<K extends string> = {
|
|
48
|
+
readonly keys: readonly K[]
|
|
49
|
+
readonly environment: string
|
|
50
|
+
readonly signal?: AbortSignal
|
|
51
|
+
readonly timeoutMs?: number
|
|
52
|
+
|
|
53
|
+
/** Report the source actually selected by a dynamic loader. */
|
|
54
|
+
readonly reportSource?: (source: string) => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type SecretSetLoader<K extends string> = ((
|
|
58
|
+
context: SecretSetLoaderContext<K>
|
|
59
|
+
) => Promise<Partial<Record<K, string>>>) & {
|
|
60
|
+
readonly source?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type SecretSet<K extends string, Parsed = Record<K, string>> = {
|
|
64
|
+
secret: (key: K) => string
|
|
65
|
+
secrets: () => Record<K, string>
|
|
66
|
+
parsed: () => Parsed
|
|
67
|
+
sources: () => Record<K, string>
|
|
68
|
+
reload: (options?: SecretRequestOptions) => Promise<SecretSet<K, Parsed>>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Shared across bundled entry points and package copies in the same JS realm.
|
|
72
|
+
// Only helper-written values are registered; original environment overrides aren't.
|
|
73
|
+
const registryKey = Symbol.for('hanzio.secrets.environmentWrites.v1')
|
|
74
|
+
const shared = globalThis as typeof globalThis & {
|
|
75
|
+
[registryKey]?: WeakMap<EnvironmentVariables, Map<string, string>>
|
|
76
|
+
}
|
|
77
|
+
const environmentWrites =
|
|
78
|
+
shared[registryKey] ??
|
|
79
|
+
new WeakMap<EnvironmentVariables, Map<string, string>>()
|
|
80
|
+
|
|
81
|
+
shared[registryKey] = environmentWrites
|
|
82
|
+
|
|
83
|
+
const defaultLogger: SecretLogger = {
|
|
84
|
+
info: (message, attrs) =>
|
|
85
|
+
console.log(
|
|
86
|
+
`${message} environment=${attrs.environment} count=${attrs.count} source=${attrs.source} durationMs=${attrs.durationMs}`,
|
|
87
|
+
{ sources: attrs.sources }
|
|
88
|
+
),
|
|
89
|
+
error: (message, attrs) => console.error(message, attrs)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readOverrides<K extends string>(
|
|
93
|
+
keys: readonly K[],
|
|
94
|
+
env?: EnvironmentVariables
|
|
95
|
+
): Partial<Record<K, string>> {
|
|
96
|
+
if (!env) {
|
|
97
|
+
return {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const writes = environmentWrites.get(env)
|
|
101
|
+
|
|
102
|
+
return Object.fromEntries(
|
|
103
|
+
keys.flatMap((key) => {
|
|
104
|
+
const value = Object.hasOwn(env, key) ? env[key] : undefined
|
|
105
|
+
|
|
106
|
+
// A matching helper write is a mirror, not an explicit user override.
|
|
107
|
+
if (writes?.has(key)) {
|
|
108
|
+
if (writes.get(key) === value) {
|
|
109
|
+
return []
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// An external change gives ownership back to the environment.
|
|
113
|
+
writes.delete(key)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return value ? [[key, value]] : []
|
|
117
|
+
})
|
|
118
|
+
) as Partial<Record<K, string>>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function defineSecretSet<
|
|
122
|
+
const Keys extends readonly string[],
|
|
123
|
+
Parsed = Record<Keys[number], string>
|
|
124
|
+
>(
|
|
125
|
+
keys: Keys,
|
|
126
|
+
options: CustomLoaderDefineSecretSetOptions<Keys[number], Parsed>,
|
|
127
|
+
environmentVariables?: EnvironmentVariables
|
|
128
|
+
): Promise<SecretSet<Keys[number], Parsed>> {
|
|
129
|
+
type K = Keys[number]
|
|
130
|
+
|
|
131
|
+
const keySet = [...new Set(keys)] as K[]
|
|
132
|
+
const environment = resolveSecretEnvironment(
|
|
133
|
+
options.environment,
|
|
134
|
+
environmentVariables
|
|
135
|
+
)
|
|
136
|
+
const logger =
|
|
137
|
+
options.logger === false ? undefined : (options.logger ?? defaultLogger)
|
|
138
|
+
|
|
139
|
+
// These snapshots change together only after an entire load succeeds.
|
|
140
|
+
let cached: Record<K, string>
|
|
141
|
+
let parsed: Parsed
|
|
142
|
+
let sources: Record<K, string>
|
|
143
|
+
let inFlight: Promise<void> | undefined
|
|
144
|
+
|
|
145
|
+
const log = (
|
|
146
|
+
level: 'info' | 'error',
|
|
147
|
+
message: string,
|
|
148
|
+
attributes: SecretLogAttributes
|
|
149
|
+
) => {
|
|
150
|
+
try {
|
|
151
|
+
logger?.[level](message, attributes)
|
|
152
|
+
} catch {
|
|
153
|
+
/* Logging cannot change a load result. */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Build, validate, and commit one snapshot under the caller’s deadline.
|
|
158
|
+
const loadSnapshot = async (
|
|
159
|
+
request: SecretRequestOptions,
|
|
160
|
+
signal: AbortSignal
|
|
161
|
+
) => {
|
|
162
|
+
// Fetch only keys that lack genuine local overrides.
|
|
163
|
+
const local = readOverrides(keySet, environmentVariables)
|
|
164
|
+
const missing = keySet.filter((key) => !Object.hasOwn(local, key))
|
|
165
|
+
let loaderSource = options.loader.source ?? 'custom loader'
|
|
166
|
+
let loaded: Partial<Record<K, string>> = {}
|
|
167
|
+
|
|
168
|
+
if (missing.length) {
|
|
169
|
+
try {
|
|
170
|
+
loaded = await options.loader({
|
|
171
|
+
keys: missing,
|
|
172
|
+
environment,
|
|
173
|
+
signal,
|
|
174
|
+
timeoutMs: request.timeoutMs ?? options.timeoutMs ?? 30_000,
|
|
175
|
+
reportSource: (source) => {
|
|
176
|
+
loaderSource = source
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error instanceof SecretLoadError) {
|
|
181
|
+
throw error
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw new SecretLoadError('LOADER', 'Secret loader failed.')
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Honor overrides changed while the loader was running.
|
|
189
|
+
const overrides = readOverrides(keySet, environmentVariables)
|
|
190
|
+
const next = Object.fromEntries(
|
|
191
|
+
keySet.map((key) => {
|
|
192
|
+
if (Object.hasOwn(overrides, key)) {
|
|
193
|
+
return [key, overrides[key]]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (Object.hasOwn(loaded, key)) {
|
|
197
|
+
return [key, loaded[key]]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return [key, undefined]
|
|
201
|
+
})
|
|
202
|
+
) as Record<K, string>
|
|
203
|
+
const missingKeys = keySet.filter(
|
|
204
|
+
(key) => typeof next[key] !== 'string' || !next[key]
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if (missingKeys.length) {
|
|
208
|
+
throw new SecretLoadError(
|
|
209
|
+
'MISSING_SECRETS',
|
|
210
|
+
`Missing secrets: ${missingKeys.join(', ')}`
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Validate the merged snapshot before touching the cache or environment.
|
|
215
|
+
let nextParsed: Parsed
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
nextParsed = options.schema
|
|
219
|
+
? await options.schema.parseAsync({ ...next })
|
|
220
|
+
: ({ ...next } as Parsed)
|
|
221
|
+
|
|
222
|
+
// Validate copyability before committing anything.
|
|
223
|
+
nextParsed = structuredClone(nextParsed)
|
|
224
|
+
} catch {
|
|
225
|
+
throw new SecretLoadError(
|
|
226
|
+
'VALIDATION',
|
|
227
|
+
'Secret schema validation failed. Check the configured schema; values are omitted.'
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// A custom loader or schema may ignore cancellation while awaiting.
|
|
232
|
+
// Its late result must never replace the last successful snapshot.
|
|
233
|
+
signal.throwIfAborted()
|
|
234
|
+
|
|
235
|
+
const nextSources = Object.fromEntries(
|
|
236
|
+
keySet.map((key) => [
|
|
237
|
+
key,
|
|
238
|
+
Object.hasOwn(overrides, key) ? 'process.env' : loaderSource
|
|
239
|
+
])
|
|
240
|
+
) as Record<K, string>
|
|
241
|
+
|
|
242
|
+
// Register only fetched values so future loads can identify our mirrors.
|
|
243
|
+
if (options.writeToProcessEnv !== false && environmentVariables) {
|
|
244
|
+
let writes = environmentWrites.get(environmentVariables)
|
|
245
|
+
|
|
246
|
+
if (!writes) {
|
|
247
|
+
writes = new Map()
|
|
248
|
+
environmentWrites.set(environmentVariables, writes)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const key of keySet) {
|
|
252
|
+
if (Object.hasOwn(overrides, key)) {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
environmentVariables[key] = next[key]
|
|
257
|
+
writes.set(key, next[key])
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
cached = next
|
|
262
|
+
parsed = nextParsed
|
|
263
|
+
sources = nextSources
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const load = (request: SecretRequestOptions = {}): Promise<void> => {
|
|
267
|
+
// Concurrent reloads share the first caller's deadline and cancellation.
|
|
268
|
+
if (inFlight) {
|
|
269
|
+
return inFlight
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
inFlight = (async () => {
|
|
273
|
+
const start = Date.now()
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
await withSecretDeadline(
|
|
277
|
+
{
|
|
278
|
+
timeoutMs: request.timeoutMs ?? options.timeoutMs,
|
|
279
|
+
signal: request.signal ?? options.signal
|
|
280
|
+
},
|
|
281
|
+
(signal) => loadSnapshot(request, signal)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
// Report the sources of committed values, not attempted fetches.
|
|
285
|
+
const sourceCounts = new Map<string, number>()
|
|
286
|
+
|
|
287
|
+
for (const key of keySet) {
|
|
288
|
+
const source = sources[key]
|
|
289
|
+
|
|
290
|
+
sourceCounts.set(source, (sourceCounts.get(source) ?? 0) + 1)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const counts = Object.fromEntries(sourceCounts)
|
|
294
|
+
|
|
295
|
+
log('info', 'Loaded secrets', {
|
|
296
|
+
environment,
|
|
297
|
+
count: keySet.length,
|
|
298
|
+
durationMs: Date.now() - start,
|
|
299
|
+
source: Object.keys(counts).join(' + ') || 'none',
|
|
300
|
+
sources: counts
|
|
301
|
+
})
|
|
302
|
+
} catch (error) {
|
|
303
|
+
const safe =
|
|
304
|
+
error instanceof SecretLoadError
|
|
305
|
+
? error
|
|
306
|
+
: new SecretLoadError('LOADER', 'Secret loading failed.')
|
|
307
|
+
|
|
308
|
+
log('error', 'Secret loading failed', {
|
|
309
|
+
environment,
|
|
310
|
+
count: keySet.length,
|
|
311
|
+
durationMs: Date.now() - start,
|
|
312
|
+
code: safe.code
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
throw safe
|
|
316
|
+
}
|
|
317
|
+
})().finally(() => {
|
|
318
|
+
inFlight = undefined
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
return inFlight
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const secretSet: SecretSet<K, Parsed> = {
|
|
325
|
+
secret(key) {
|
|
326
|
+
if (!Object.hasOwn(cached, key)) {
|
|
327
|
+
throw new SecretLoadError('MISSING_SECRETS', `Secret ${key} not found`)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return cached[key]
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
// Return copies so consumers cannot mutate the internal snapshots.
|
|
334
|
+
secrets: () => ({ ...cached }),
|
|
335
|
+
parsed: () => structuredClone(parsed),
|
|
336
|
+
sources: () => ({ ...sources }),
|
|
337
|
+
|
|
338
|
+
async reload(request) {
|
|
339
|
+
await load(request)
|
|
340
|
+
|
|
341
|
+
return secretSet
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await load()
|
|
346
|
+
|
|
347
|
+
return secretSet
|
|
348
|
+
}
|
|
@@ -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
|
+
}
|