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.
Files changed (172) hide show
  1. package/README.md +832 -219
  2. package/dist/api-wrapper/builders.d.ts +15 -3
  3. package/dist/api-wrapper/create-client.d.ts +1 -1
  4. package/dist/api-wrapper/describe.d.ts +16 -0
  5. package/dist/api-wrapper/endpoint.d.ts +18 -0
  6. package/dist/api-wrapper/errors.d.ts +40 -2
  7. package/dist/api-wrapper/headers.d.ts +2 -1
  8. package/dist/api-wrapper/index.d.ts +5 -0
  9. package/dist/api-wrapper/otel.d.ts +44 -0
  10. package/dist/api-wrapper/paginate.d.ts +12 -0
  11. package/dist/api-wrapper/redaction.d.ts +2 -0
  12. package/dist/api-wrapper/shared.d.ts +16 -2
  13. package/dist/api-wrapper/testing.d.ts +23 -0
  14. package/dist/api-wrapper/transport.d.ts +12 -10
  15. package/dist/api-wrapper/types.d.ts +104 -18
  16. package/dist/array/index.d.ts +50 -4
  17. package/dist/cache/createCache.d.ts +31 -0
  18. package/dist/cache/index.d.ts +45 -2
  19. package/dist/color/contrast.d.ts +40 -0
  20. package/dist/color/index.d.ts +2 -0
  21. package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
  22. package/dist/cool-console-log/index.d.ts +34 -5
  23. package/dist/crypto/encoding.d.ts +18 -0
  24. package/dist/crypto/hash.d.ts +33 -0
  25. package/dist/crypto/index.d.ts +4 -0
  26. package/dist/crypto/sealer.d.ts +25 -0
  27. package/dist/crypto/webhook.d.ts +20 -0
  28. package/dist/date/index.d.ts +46 -2
  29. package/dist/emitter/index.d.ts +21 -0
  30. package/dist/error/index.d.ts +12 -0
  31. package/dist/format/index.d.ts +102 -0
  32. package/dist/function/index.d.ts +60 -0
  33. package/dist/guard/index.d.ts +20 -0
  34. package/dist/i18n/index.d.ts +37 -0
  35. package/dist/index-0zvtcbzy.js +224 -0
  36. package/dist/index-181e6vfw.js +29 -0
  37. package/dist/index-8r2vdzjg.js +87 -0
  38. package/dist/index-hy7kbb99.js +157 -0
  39. package/dist/{src/secrets/loaders.js → index-j1ybktbr.js} +608 -405
  40. package/dist/index-k9yx6bne.js +89 -0
  41. package/dist/index-kj1phgrx.js +169 -0
  42. package/dist/index-mnkcfyx8.js +78 -0
  43. package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
  44. package/dist/index-rp5pqghz.js +157 -0
  45. package/dist/index-srt06xd2.js +15 -0
  46. package/dist/index-wjyb9bsd.js +263 -0
  47. package/dist/index.d.ts +7 -7
  48. package/dist/jwt/index.d.ts +50 -2
  49. package/dist/math/index.d.ts +2 -2
  50. package/dist/math/linearRegressionTrend.d.ts +14 -1
  51. package/dist/math/scale.d.ts +8 -0
  52. package/dist/object/index.d.ts +19 -0
  53. package/dist/p-queue/index.d.ts +1 -1
  54. package/dist/promise/index.d.ts +97 -2
  55. package/dist/secrets/core.d.ts +47 -0
  56. package/dist/secrets/deadline.d.ts +6 -0
  57. package/dist/secrets/environment.d.ts +9 -0
  58. package/dist/secrets/errors.d.ts +11 -0
  59. package/dist/secrets/index.d.ts +13 -25
  60. package/dist/secrets/infisical-cli.d.ts +6 -0
  61. package/dist/secrets/infisical-config.d.ts +17 -0
  62. package/dist/secrets/infisical-session.d.ts +4 -0
  63. package/dist/secrets/infisical.d.ts +8 -13
  64. package/dist/secrets/loaders.d.ts +5 -12
  65. package/dist/secrets/vite.d.ts +7 -3
  66. package/dist/secrets/worker-loader.d.ts +9 -0
  67. package/dist/secrets/worker.d.ts +3 -0
  68. package/dist/sitemap/index.d.ts +48 -6
  69. package/dist/src/api-wrapper/index.js +60 -1100
  70. package/dist/src/api-wrapper/testing.js +20 -0
  71. package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
  72. package/dist/src/crypto/index.js +195 -0
  73. package/dist/src/format/index.js +211 -0
  74. package/dist/src/i18n/index.js +36 -0
  75. package/dist/src/index.js +863 -2809
  76. package/dist/src/jwt/index.js +138 -92
  77. package/dist/src/p-queue/index.js +9 -437
  78. package/dist/src/secrets/index.js +261 -1377
  79. package/dist/src/secrets/vite.js +6 -17
  80. package/dist/src/secrets/worker.js +12 -0
  81. package/dist/src/sitemap/index.js +183 -511
  82. package/dist/src/storage/index.js +119 -0
  83. package/dist/src/zod/index.js +347 -174
  84. package/dist/state/index.d.ts +21 -0
  85. package/dist/storage/index.d.ts +58 -0
  86. package/dist/string/index.d.ts +75 -2
  87. package/dist/typedswitch/index.d.ts +37 -26
  88. package/dist/types/index.d.ts +11 -0
  89. package/dist/url/index.d.ts +50 -1
  90. package/dist/zod/index.d.ts +32 -3
  91. package/dist/zod/introspect.d.ts +19 -0
  92. package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
  93. package/dist/zod/zodMinMaxFilter.d.ts +22 -3
  94. package/dist/zod/zodToTypeString.d.ts +8 -3
  95. package/package.json +87 -7
  96. package/src/api-wrapper/actions-impl.ts +11 -72
  97. package/src/api-wrapper/builders.ts +84 -52
  98. package/src/api-wrapper/create-client.ts +246 -96
  99. package/src/api-wrapper/describe.ts +157 -0
  100. package/src/api-wrapper/endpoint.ts +26 -0
  101. package/src/api-wrapper/error-reporting.ts +7 -1
  102. package/src/api-wrapper/errors.ts +117 -2
  103. package/src/api-wrapper/headers.ts +19 -1
  104. package/src/api-wrapper/index.ts +5 -0
  105. package/src/api-wrapper/otel.ts +139 -0
  106. package/src/api-wrapper/paginate.ts +28 -0
  107. package/src/api-wrapper/redaction.ts +43 -0
  108. package/src/api-wrapper/shared.ts +21 -2
  109. package/src/api-wrapper/testing.ts +48 -0
  110. package/src/api-wrapper/transport.ts +68 -74
  111. package/src/api-wrapper/types.ts +186 -18
  112. package/src/array/index.ts +195 -42
  113. package/src/cache/createCache.ts +157 -0
  114. package/src/cache/index.ts +235 -83
  115. package/src/color/contrast.ts +163 -0
  116. package/src/color/index.ts +22 -0
  117. package/src/{math → color}/metricsSeriesColors.ts +47 -49
  118. package/src/cool-console-log/index.ts +128 -41
  119. package/src/crypto/encoding.ts +83 -0
  120. package/src/crypto/hash.ts +170 -0
  121. package/src/crypto/index.ts +26 -0
  122. package/src/crypto/sealer.ts +168 -0
  123. package/src/crypto/webhook.ts +86 -0
  124. package/src/date/index.ts +109 -34
  125. package/src/emitter/index.ts +82 -0
  126. package/src/error/index.ts +63 -0
  127. package/src/format/index.ts +389 -0
  128. package/src/function/index.ts +201 -0
  129. package/src/guard/index.ts +80 -0
  130. package/src/i18n/index.ts +125 -0
  131. package/src/index.ts +11 -7
  132. package/src/jwt/index.ts +199 -125
  133. package/src/math/index.ts +5 -10
  134. package/src/math/linearRegressionTrend.ts +33 -8
  135. package/src/math/scale.ts +17 -0
  136. package/src/object/index.ts +99 -0
  137. package/src/p-queue/index.ts +2 -0
  138. package/src/promise/index.ts +304 -9
  139. package/src/secrets/core.ts +348 -0
  140. package/src/secrets/deadline.ts +69 -0
  141. package/src/secrets/environment.ts +69 -0
  142. package/src/secrets/errors.ts +29 -0
  143. package/src/secrets/index.ts +79 -210
  144. package/src/secrets/infisical-cli.ts +99 -0
  145. package/src/secrets/infisical-config.ts +70 -0
  146. package/src/secrets/infisical-session.ts +42 -0
  147. package/src/secrets/infisical.ts +214 -66
  148. package/src/secrets/loaders.ts +24 -48
  149. package/src/secrets/vite.ts +44 -15
  150. package/src/secrets/worker-loader.ts +56 -0
  151. package/src/secrets/worker.ts +4 -0
  152. package/src/sitemap/index.ts +293 -130
  153. package/src/state/index.ts +133 -81
  154. package/src/storage/index.ts +187 -0
  155. package/src/string/index.ts +261 -23
  156. package/src/typedswitch/index.ts +134 -103
  157. package/src/types/index.ts +14 -0
  158. package/src/url/index.ts +146 -1
  159. package/src/zod/index.ts +105 -9
  160. package/src/zod/introspect.ts +103 -0
  161. package/src/zod/jsonToZodSchemaMapper.ts +44 -21
  162. package/src/zod/zodMinMaxFilter.ts +37 -13
  163. package/src/zod/zodToTypeString.ts +184 -181
  164. package/dist/api-wrapper/index.js +0 -250
  165. package/dist/gzip/index.d.ts +0 -2
  166. package/dist/gzip/index.js +0 -16
  167. package/dist/index.js +0 -1823
  168. package/dist/jwt/index.js +0 -19
  169. package/dist/sitemap/index.js +0 -552
  170. package/dist/src/gzip/index.js +0 -16
  171. package/dist/src/secrets/infisical.js +0 -1194
  172. 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
+ }