hanzio 1.2.0 → 2.1.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 (155) hide show
  1. package/README.md +786 -239
  2. package/dist/api-wrapper/builders.d.ts +4 -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 +39 -2
  7. package/dist/api-wrapper/index.d.ts +5 -0
  8. package/dist/api-wrapper/otel.d.ts +44 -0
  9. package/dist/api-wrapper/paginate.d.ts +12 -0
  10. package/dist/api-wrapper/redaction.d.ts +2 -0
  11. package/dist/api-wrapper/shared.d.ts +16 -2
  12. package/dist/api-wrapper/testing.d.ts +23 -0
  13. package/dist/api-wrapper/transport.d.ts +4 -5
  14. package/dist/api-wrapper/types.d.ts +103 -18
  15. package/dist/array/index.d.ts +50 -4
  16. package/dist/cache/createCache.d.ts +31 -0
  17. package/dist/cache/index.d.ts +19 -2
  18. package/dist/color/contrast.d.ts +40 -0
  19. package/dist/color/index.d.ts +2 -0
  20. package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
  21. package/dist/cool-console-log/index.d.ts +34 -5
  22. package/dist/crypto/encoding.d.ts +18 -0
  23. package/dist/crypto/hash.d.ts +33 -0
  24. package/dist/crypto/index.d.ts +4 -0
  25. package/dist/crypto/sealer.d.ts +25 -0
  26. package/dist/crypto/webhook.d.ts +20 -0
  27. package/dist/date/index.d.ts +46 -2
  28. package/dist/emitter/index.d.ts +21 -0
  29. package/dist/error/index.d.ts +12 -0
  30. package/dist/format/index.d.ts +102 -0
  31. package/dist/function/index.d.ts +60 -0
  32. package/dist/guard/index.d.ts +20 -0
  33. package/dist/i18n/index.d.ts +37 -0
  34. package/dist/index-0zvtcbzy.js +224 -0
  35. package/dist/index-8r2vdzjg.js +87 -0
  36. package/dist/index-hy7kbb99.js +157 -0
  37. package/dist/index-hyt6c56h.js +290 -0
  38. package/dist/{src/secrets/infisical.js → index-j1ybktbr.js} +500 -538
  39. package/dist/index-k9yx6bne.js +89 -0
  40. package/dist/index-kj1phgrx.js +169 -0
  41. package/dist/index-ktynw3vs.js +37 -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-w882zry3.js +29 -0
  46. package/dist/index.d.ts +7 -7
  47. package/dist/jwt/index.d.ts +50 -2
  48. package/dist/math/index.d.ts +2 -2
  49. package/dist/math/linearRegressionTrend.d.ts +14 -1
  50. package/dist/math/scale.d.ts +8 -0
  51. package/dist/object/index.d.ts +19 -0
  52. package/dist/p-queue/index.d.ts +1 -1
  53. package/dist/promise/index.d.ts +97 -2
  54. package/dist/secrets/core.d.ts +7 -0
  55. package/dist/secrets/index.d.ts +3 -1
  56. package/dist/secrets/public-config.d.ts +7 -0
  57. package/dist/secrets/vite.d.ts +1 -0
  58. package/dist/secrets/worker.d.ts +1 -0
  59. package/dist/sitemap/index.d.ts +48 -6
  60. package/dist/src/api-wrapper/index.js +60 -1130
  61. package/dist/src/api-wrapper/testing.js +20 -0
  62. package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
  63. package/dist/src/crypto/index.js +195 -0
  64. package/dist/src/format/index.js +211 -0
  65. package/dist/src/i18n/index.js +36 -0
  66. package/dist/src/index.js +739 -3145
  67. package/dist/src/jwt/index.js +138 -92
  68. package/dist/src/p-queue/index.js +9 -437
  69. package/dist/src/secrets/index.js +50 -1428
  70. package/dist/src/secrets/vite.js +10 -39
  71. package/dist/src/secrets/worker.js +14 -273
  72. package/dist/src/sitemap/index.js +183 -511
  73. package/dist/src/storage/index.js +119 -0
  74. package/dist/src/zod/index.js +340 -167
  75. package/dist/state/index.d.ts +21 -0
  76. package/dist/storage/index.d.ts +58 -0
  77. package/dist/string/index.d.ts +75 -2
  78. package/dist/typedswitch/index.d.ts +31 -24
  79. package/dist/types/index.d.ts +11 -0
  80. package/dist/url/index.d.ts +50 -1
  81. package/dist/zod/index.d.ts +32 -3
  82. package/dist/zod/introspect.d.ts +19 -0
  83. package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
  84. package/dist/zod/zodMinMaxFilter.d.ts +22 -3
  85. package/dist/zod/zodToTypeString.d.ts +8 -3
  86. package/package.json +78 -7
  87. package/src/api-wrapper/actions-impl.ts +11 -72
  88. package/src/api-wrapper/builders.ts +32 -25
  89. package/src/api-wrapper/create-client.ts +171 -41
  90. package/src/api-wrapper/describe.ts +157 -0
  91. package/src/api-wrapper/endpoint.ts +26 -0
  92. package/src/api-wrapper/error-reporting.ts +7 -1
  93. package/src/api-wrapper/errors.ts +107 -2
  94. package/src/api-wrapper/headers.ts +11 -6
  95. package/src/api-wrapper/index.ts +5 -0
  96. package/src/api-wrapper/otel.ts +139 -0
  97. package/src/api-wrapper/paginate.ts +28 -0
  98. package/src/api-wrapper/redaction.ts +42 -0
  99. package/src/api-wrapper/shared.ts +21 -2
  100. package/src/api-wrapper/testing.ts +48 -0
  101. package/src/api-wrapper/transport.ts +41 -43
  102. package/src/api-wrapper/types.ts +185 -18
  103. package/src/array/index.ts +195 -42
  104. package/src/cache/createCache.ts +157 -0
  105. package/src/cache/index.ts +132 -147
  106. package/src/color/contrast.ts +163 -0
  107. package/src/color/index.ts +22 -0
  108. package/src/{math → color}/metricsSeriesColors.ts +47 -49
  109. package/src/cool-console-log/index.ts +128 -41
  110. package/src/crypto/encoding.ts +83 -0
  111. package/src/crypto/hash.ts +170 -0
  112. package/src/crypto/index.ts +26 -0
  113. package/src/crypto/sealer.ts +168 -0
  114. package/src/crypto/webhook.ts +86 -0
  115. package/src/date/index.ts +109 -34
  116. package/src/emitter/index.ts +82 -0
  117. package/src/error/index.ts +63 -0
  118. package/src/format/index.ts +389 -0
  119. package/src/function/index.ts +201 -0
  120. package/src/guard/index.ts +80 -0
  121. package/src/i18n/index.ts +125 -0
  122. package/src/index.ts +11 -7
  123. package/src/jwt/index.ts +199 -125
  124. package/src/math/index.ts +5 -10
  125. package/src/math/linearRegressionTrend.ts +33 -8
  126. package/src/math/scale.ts +17 -0
  127. package/src/object/index.ts +99 -0
  128. package/src/p-queue/index.ts +2 -0
  129. package/src/promise/index.ts +304 -9
  130. package/src/secrets/core.ts +72 -16
  131. package/src/secrets/index.ts +16 -4
  132. package/src/secrets/public-config.ts +47 -0
  133. package/src/secrets/vite.ts +2 -0
  134. package/src/secrets/worker.ts +2 -0
  135. package/src/sitemap/index.ts +293 -130
  136. package/src/state/index.ts +133 -81
  137. package/src/storage/index.ts +187 -0
  138. package/src/string/index.ts +261 -23
  139. package/src/typedswitch/index.ts +58 -38
  140. package/src/types/index.ts +14 -0
  141. package/src/url/index.ts +146 -1
  142. package/src/zod/index.ts +105 -9
  143. package/src/zod/introspect.ts +103 -0
  144. package/src/zod/jsonToZodSchemaMapper.ts +44 -21
  145. package/src/zod/zodMinMaxFilter.ts +37 -13
  146. package/src/zod/zodToTypeString.ts +184 -181
  147. package/dist/api-wrapper/index.js +0 -250
  148. package/dist/gzip/index.d.ts +0 -2
  149. package/dist/gzip/index.js +0 -16
  150. package/dist/index.js +0 -1823
  151. package/dist/jwt/index.js +0 -19
  152. package/dist/sitemap/index.js +0 -552
  153. package/dist/src/gzip/index.js +0 -16
  154. package/dist/src/secrets/loaders.js +0 -1529
  155. package/dist/zod/index.js +0 -210
@@ -0,0 +1,99 @@
1
+ /** `Object.keys` typed as the object's string keys. */
2
+ export function typedKeys<T extends object>(object: T): (keyof T & string)[] {
3
+ return Object.keys(object) as (keyof T & string)[]
4
+ }
5
+
6
+ /** `Object.entries` typed as `[key, value]` pairs per key. */
7
+ export function typedEntries<T extends object>(
8
+ object: T
9
+ ): { [K in keyof T & string]: [K, T[K]] }[keyof T & string][] {
10
+ return Object.entries(object) as {
11
+ [K in keyof T & string]: [K, T[K]]
12
+ }[keyof T & string][]
13
+ }
14
+
15
+ /** `Object.fromEntries` typed as `Record<K, V>`. */
16
+ export function typedFromEntries<const K extends PropertyKey, V>(
17
+ entries: Iterable<readonly [K, V]>
18
+ ): Record<K, V> {
19
+ return Object.fromEntries(entries) as Record<K, V>
20
+ }
21
+
22
+ /** Builds a record with one entry per key. */
23
+ export function recordFromKeys<const K extends PropertyKey, V>(
24
+ keys: readonly K[],
25
+ fn: (key: K) => V
26
+ ): Record<K, V> {
27
+ return typedFromEntries(keys.map((key) => [key, fn(key)] as const))
28
+ }
29
+
30
+ export function mapValues<T extends object, R>(
31
+ object: T,
32
+ fn: (value: T[keyof T & string], key: keyof T & string) => R
33
+ ): Record<keyof T & string, R> {
34
+ return typedFromEntries(
35
+ typedEntries(object).map(([key, value]) => [key, fn(value, key)] as const)
36
+ )
37
+ }
38
+
39
+ export function pick<T extends object, const K extends keyof T>(
40
+ object: T,
41
+ keys: readonly K[]
42
+ ): Pick<T, K> {
43
+ const result = {} as Pick<T, K>
44
+ for (const key of keys) {
45
+ if (key in object) result[key] = object[key]
46
+ }
47
+ return result
48
+ }
49
+
50
+ export function omit<T extends object, const K extends keyof T>(
51
+ object: T,
52
+ keys: readonly K[]
53
+ ): Omit<T, K> {
54
+ const result = { ...object }
55
+ for (const key of keys) delete result[key]
56
+ return result
57
+ }
58
+
59
+ type Path = string | readonly (string | number)[]
60
+
61
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
62
+
63
+ function toPath(path: Path): readonly (string | number)[] {
64
+ if (typeof path !== 'string') return path
65
+ return path === '' ? [] : path.split('.')
66
+ }
67
+
68
+ /** Reads a nested value by dot path (`a.b.0.c`) or segment array; undefined when missing. */
69
+ export function getAtPath(object: unknown, path: Path): unknown {
70
+ let current = object
71
+ for (const key of toPath(path)) {
72
+ if (current === null || typeof current !== 'object') return undefined
73
+ current = (current as Record<PropertyKey, unknown>)[key]
74
+ }
75
+ return current
76
+ }
77
+
78
+ /** Writes a nested value in place, creating arrays for numeric segments and objects otherwise. */
79
+ export function setAtPath(object: object, path: Path, value: unknown): void {
80
+ const keys = toPath(path)
81
+ if (keys.length === 0) throw new Error('Path must not be empty')
82
+ for (const key of keys) {
83
+ if (UNSAFE_KEYS.has(String(key))) {
84
+ throw new Error(`Unsafe path segment: ${key}`)
85
+ }
86
+ }
87
+
88
+ let current = object as Record<PropertyKey, unknown>
89
+ for (let index = 0; index < keys.length - 1; index++) {
90
+ const key = keys[index]!
91
+ let next = current[key]
92
+ if (next === null || typeof next !== 'object') {
93
+ next = /^\d+$/.test(String(keys[index + 1])) ? [] : {}
94
+ current[key] = next
95
+ }
96
+ current = next as Record<PropertyKey, unknown>
97
+ }
98
+ current[keys.at(-1)!] = value
99
+ }
@@ -688,3 +688,5 @@ export default class PQueue<
688
688
  }
689
689
  }
690
690
  }
691
+
692
+ export { PQueue }
@@ -5,27 +5,37 @@ export type BackgroundPromiseOptions = {
5
5
  }
6
6
 
7
7
  export type Success<T> = {
8
+ ok: true
8
9
  data: T
9
10
  error: null
10
11
  }
11
12
 
12
13
  export type Failure<E> = {
14
+ ok: false
13
15
  data: null
14
16
  error: E
15
17
  }
16
18
 
17
19
  export type Result<T, E = Error> = Success<T> | Failure<E>
18
20
 
21
+ export function ok<T>(data: T): Success<T> {
22
+ return { ok: true, data, error: null }
23
+ }
24
+
25
+ export function err<E>(error: E): Failure<E> {
26
+ return { ok: false, data: null, error }
27
+ }
28
+
19
29
  export function isSuccess<T, E>(result: Result<T, E>): result is Success<T> {
20
- return result.error === null
30
+ return result.ok
21
31
  }
22
32
 
23
33
  export function isFailure<T, E>(result: Result<T, E>): result is Failure<E> {
24
- return result.error !== null
34
+ return !result.ok
25
35
  }
26
36
 
27
37
  export function unwrapResult<T, E>(result: Result<T, E>): T {
28
- if (isSuccess(result)) return result.data
38
+ if (result.ok) return result.data
29
39
  throw result.error
30
40
  }
31
41
 
@@ -57,17 +67,302 @@ export function backgroundPromiseSync<T>(
57
67
  })
58
68
  }
59
69
 
60
- export function promiseTimeout(ms: number): Promise<void> {
61
- return new Promise((resolve) => setTimeout(resolve, ms))
70
+ /** Resolves after `ms`; rejects with `signal.reason` if the signal aborts first. */
71
+ export function promiseTimeout(
72
+ ms: number,
73
+ options: { signal?: AbortSignal } = {}
74
+ ): Promise<void> {
75
+ const { signal } = options
76
+ return new Promise((resolve, reject) => {
77
+ if (signal?.aborted) {
78
+ reject(signal.reason)
79
+ return
80
+ }
81
+ const onAbort = () => {
82
+ clearTimeout(timer)
83
+ reject(signal?.reason)
84
+ }
85
+ const timer = setTimeout(() => {
86
+ signal?.removeEventListener('abort', onAbort)
87
+ resolve()
88
+ }, ms)
89
+ signal?.addEventListener('abort', onAbort, { once: true })
90
+ })
62
91
  }
63
92
 
93
+ /**
94
+ * Accepts a promise or a function; sync throws inside the function are
95
+ * captured too.
96
+ */
64
97
  export async function tryCatch<T, E = Error>(
65
- promise: Promise<T>
98
+ input: Promise<T> | (() => T | Promise<T>)
66
99
  ): Promise<Result<T, E>> {
67
100
  try {
68
- const data = await promise
69
- return { data, error: null }
101
+ return ok(await (typeof input === 'function' ? input() : input))
70
102
  } catch (error) {
71
- return { data: null, error: error as E }
103
+ return err(error as E)
104
+ }
105
+ }
106
+
107
+ export class PromiseTimeoutError extends Error {
108
+ readonly timeoutMs: number
109
+
110
+ constructor(timeoutMs: number, message?: string) {
111
+ super(message ?? `Promise timed out after ${timeoutMs}ms`)
112
+ this.name = 'PromiseTimeoutError'
113
+ this.timeoutMs = timeoutMs
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Rejects with `PromiseTimeoutError` after `ms`, or with `signal.reason` when
119
+ * the signal aborts. Pass a function to receive a signal that aborts on
120
+ * timeout, so the underlying work can stop too.
121
+ */
122
+ export async function withTimeout<T>(
123
+ input: Promise<T> | ((signal: AbortSignal) => Promise<T>),
124
+ ms: number,
125
+ options: { signal?: AbortSignal; message?: string } = {}
126
+ ): Promise<T> {
127
+ const controller = new AbortController()
128
+ const onAbort = () => controller.abort(options.signal?.reason)
129
+ if (options.signal?.aborted) onAbort()
130
+ else options.signal?.addEventListener('abort', onAbort, { once: true })
131
+
132
+ const timer = setTimeout(
133
+ () => controller.abort(new PromiseTimeoutError(ms, options.message)),
134
+ ms
135
+ )
136
+ let rejectOnAbort: () => void = () => {}
137
+
138
+ try {
139
+ const aborted = new Promise<never>((_, reject) => {
140
+ rejectOnAbort = () => reject(controller.signal.reason)
141
+ controller.signal.addEventListener('abort', rejectOnAbort, { once: true })
142
+ })
143
+ if (controller.signal.aborted) throw controller.signal.reason
144
+
145
+ const promise =
146
+ typeof input === 'function' ? input(controller.signal) : input
147
+ return await Promise.race([promise, aborted])
148
+ } finally {
149
+ clearTimeout(timer)
150
+ options.signal?.removeEventListener('abort', onAbort)
151
+ controller.signal.removeEventListener('abort', rejectOnAbort)
152
+ }
153
+ }
154
+
155
+ export type BackoffOptions = {
156
+ /** Delay for the first retry (default 300ms). */
157
+ baseMs?: number
158
+ /** Upper bound for any delay (default 30s). */
159
+ maxMs?: number
160
+ /**
161
+ * `true`/`'full'`: random delay in [0, computed] (default).
162
+ * `'partial'`: computed plus 0–`jitterRatio` extra (may exceed `maxMs`).
163
+ */
164
+ jitter?: boolean | 'full' | 'partial'
165
+ /** Max extra fraction for `'partial'` jitter (default 0.25). */
166
+ jitterRatio?: number
167
+ /** Source of randomness in [0, 1) (default `Math.random`). */
168
+ random?: () => number
169
+ }
170
+
171
+ /** Exponential backoff delay for a 1-based retry attempt. */
172
+ export function exponentialBackoff(
173
+ attempt: number,
174
+ options: BackoffOptions = {}
175
+ ): number {
176
+ const {
177
+ baseMs = 300,
178
+ maxMs = 30_000,
179
+ jitter = true,
180
+ jitterRatio = 0.25,
181
+ random = Math.random
182
+ } = options
183
+ const delay = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt - 1))
184
+ if (jitter === 'partial')
185
+ return Math.round(delay * (1 + jitterRatio * random()))
186
+ return jitter ? Math.round(random() * delay) : delay
187
+ }
188
+
189
+ export type RetryOptions = {
190
+ /** Retries after the first attempt (default 3). */
191
+ retries?: number
192
+ /** Delay before retry `attempt` (1-based). Default: `exponentialBackoff`. */
193
+ delayMs?: number | ((attempt: number, error: unknown) => number)
194
+ /** Return false to stop retrying and rethrow (default: always retry). */
195
+ shouldRetry?: (error: unknown, attempt: number) => boolean
196
+ onRetry?: (error: unknown, attempt: number, delayMs: number) => void
197
+ signal?: AbortSignal
198
+ }
199
+
200
+ export async function retry<T>(
201
+ fn: (context: { attempt: number; signal?: AbortSignal }) => Promise<T> | T,
202
+ options: RetryOptions = {}
203
+ ): Promise<T> {
204
+ const { retries = 3, delayMs, shouldRetry, onRetry, signal } = options
205
+
206
+ for (let attempt = 0; ; attempt++) {
207
+ signal?.throwIfAborted()
208
+ try {
209
+ return await fn({ attempt, signal })
210
+ } catch (error) {
211
+ const nextAttempt = attempt + 1
212
+ if (attempt >= retries || shouldRetry?.(error, nextAttempt) === false) {
213
+ throw error
214
+ }
215
+ const delay =
216
+ delayMs === undefined
217
+ ? exponentialBackoff(nextAttempt)
218
+ : typeof delayMs === 'function'
219
+ ? delayMs(nextAttempt, error)
220
+ : delayMs
221
+ onRetry?.(error, nextAttempt, delay)
222
+ await promiseTimeout(delay, { signal })
223
+ }
224
+ }
225
+ }
226
+
227
+ /** Maps with bounded concurrency; results keep input order. */
228
+ export async function pMap<T, R>(
229
+ items: Iterable<T>,
230
+ fn: (item: T, index: number) => Promise<R> | R,
231
+ options: { concurrency?: number; signal?: AbortSignal } = {}
232
+ ): Promise<R[]> {
233
+ const { concurrency = Number.POSITIVE_INFINITY, signal } = options
234
+ if (!(concurrency >= 1)) {
235
+ throw new Error('Concurrency must be at least 1')
236
+ }
237
+
238
+ const list = [...items]
239
+ const results = new Array<R>(list.length)
240
+ let next = 0
241
+
242
+ const worker = async () => {
243
+ while (next < list.length) {
244
+ signal?.throwIfAborted()
245
+ const index = next++
246
+ results[index] = await fn(list[index]!, index)
247
+ }
248
+ }
249
+
250
+ await Promise.all(
251
+ Array.from({ length: Math.min(concurrency, list.length) }, worker)
252
+ )
253
+ return results
254
+ }
255
+
256
+ const clock = (): number => globalThis.performance?.now() ?? Date.now()
257
+
258
+ /** Starts a clock; call the returned function to read elapsed ms. */
259
+ export function startTimer(): () => number {
260
+ const start = clock()
261
+ return () => clock() - start
262
+ }
263
+
264
+ /** Runs `fn` and reports how long it took (throws pass through). */
265
+ export async function timed<T>(
266
+ fn: () => Promise<T> | T
267
+ ): Promise<{ result: T; durationMs: number }> {
268
+ const elapsed = startTimer()
269
+ const result = await fn()
270
+ return { result, durationMs: elapsed() }
271
+ }
272
+
273
+ /**
274
+ * Aborts when any input aborts, with that signal's `reason`. Undefined inputs
275
+ * are skipped; no inputs gives a signal that never aborts.
276
+ */
277
+ export function anySignal(
278
+ ...signals: (AbortSignal | undefined)[]
279
+ ): AbortSignal {
280
+ const list = signals.filter((signal) => signal !== undefined)
281
+ if (list.length === 1) return list[0]!
282
+ if (typeof AbortSignal.any === 'function') return AbortSignal.any(list)
283
+
284
+ // ponytail: fallback keeps listeners on inputs until one aborts (no WeakRef).
285
+ const controller = new AbortController()
286
+ const aborted = list.find((signal) => signal.aborted)
287
+ if (aborted) {
288
+ controller.abort(aborted.reason)
289
+ return controller.signal
290
+ }
291
+ const onAbort = (event: Event) => {
292
+ for (const signal of list) signal.removeEventListener('abort', onAbort)
293
+ controller.abort((event.target as AbortSignal).reason)
294
+ }
295
+ for (const signal of list) signal.addEventListener('abort', onAbort)
296
+ return controller.signal
297
+ }
298
+
299
+ /** Aborts with a `TimeoutError` after `ms`, or earlier if `signal` aborts. */
300
+ export function timeoutSignal(ms: number, signal?: AbortSignal): AbortSignal {
301
+ if (typeof AbortSignal.timeout === 'function') {
302
+ return anySignal(AbortSignal.timeout(ms), signal)
303
+ }
304
+ const controller = new AbortController()
305
+ setTimeout(() => {
306
+ const reason = new Error('The operation timed out.')
307
+ reason.name = 'TimeoutError'
308
+ controller.abort(reason)
309
+ }, ms)
310
+ return anySignal(controller.signal, signal)
311
+ }
312
+
313
+ /**
314
+ * Settles like `promise`, but rejects with `signal.reason` as soon as the
315
+ * signal aborts. The underlying work is not cancelled.
316
+ */
317
+ export async function raceAbort<T>(
318
+ promise: Promise<T>,
319
+ signal: AbortSignal | undefined
320
+ ): Promise<T> {
321
+ if (!signal) return promise
322
+ let onAbort = () => {}
323
+ const aborted = new Promise<never>((_, reject) => {
324
+ onAbort = () => reject(signal.reason)
325
+ if (signal.aborted) onAbort()
326
+ else signal.addEventListener('abort', onAbort, { once: true })
327
+ })
328
+ try {
329
+ return await Promise.race([aborted, promise])
330
+ } finally {
331
+ signal.removeEventListener('abort', onAbort)
332
+ }
333
+ }
334
+
335
+ export type LatestGuard = {
336
+ /** Starts a new generation; earlier tokens become stale. */
337
+ next(): number
338
+ isCurrent(token: number): boolean
339
+ /** Marks every outstanding token stale. */
340
+ invalidate(): void
341
+ /** Runs `fn` under a new token; stale results and errors become `{ stale: true }`. */
342
+ run<T>(
343
+ fn: () => Promise<T> | T
344
+ ): Promise<{ stale: true } | { stale: false; value: T }>
345
+ }
346
+
347
+ /** Ignores results of async work superseded by a newer call. */
348
+ export function createLatestGuard(): LatestGuard {
349
+ let current = 0
350
+ const isCurrent = (token: number) => token === current
351
+ return {
352
+ next: () => ++current,
353
+ isCurrent,
354
+ invalidate: () => {
355
+ current++
356
+ },
357
+ async run(fn) {
358
+ const token = ++current
359
+ try {
360
+ const value = await fn()
361
+ return isCurrent(token) ? { stale: false, value } : { stale: true }
362
+ } catch (error) {
363
+ if (!isCurrent(token)) return { stale: true }
364
+ throw error
365
+ }
366
+ }
72
367
  }
73
368
  }
@@ -68,6 +68,15 @@ export type SecretSet<K extends string, Parsed = Record<K, string>> = {
68
68
  reload: (options?: SecretRequestOptions) => Promise<SecretSet<K, Parsed>>
69
69
  }
70
70
 
71
+ /** A declaration that must be loaded explicitly before its values can be read. */
72
+ export type LazySecretSet<
73
+ K extends string,
74
+ Parsed = Record<K, string>
75
+ > = SecretSet<K, Parsed> & {
76
+ /** Initialize once. Concurrent calls share a load; use reload to refresh. */
77
+ load: (options?: SecretRequestOptions) => Promise<LazySecretSet<K, Parsed>>
78
+ }
79
+
71
80
  // Shared across bundled entry points and package copies in the same JS realm.
72
81
  // Only helper-written values are registered; original environment overrides aren't.
73
82
  const registryKey = Symbol.for('hanzio.secrets.environmentWrites.v1')
@@ -118,21 +127,19 @@ function readOverrides<K extends string>(
118
127
  ) as Partial<Record<K, string>>
119
128
  }
120
129
 
121
- export async function defineSecretSet<
130
+ export function createSecretSet<
122
131
  const Keys extends readonly string[],
123
132
  Parsed = Record<Keys[number], string>
124
133
  >(
125
134
  keys: Keys,
126
135
  options: CustomLoaderDefineSecretSetOptions<Keys[number], Parsed>,
127
136
  environmentVariables?: EnvironmentVariables
128
- ): Promise<SecretSet<Keys[number], Parsed>> {
137
+ ): LazySecretSet<Keys[number], Parsed> {
129
138
  type K = Keys[number]
130
139
 
131
140
  const keySet = [...new Set(keys)] as K[]
132
- const environment = resolveSecretEnvironment(
133
- options.environment,
134
- environmentVariables
135
- )
141
+ let environment: string | undefined
142
+ let initialized = false
136
143
  const logger =
137
144
  options.logger === false ? undefined : (options.logger ?? defaultLogger)
138
145
 
@@ -157,7 +164,8 @@ export async function defineSecretSet<
157
164
  // Build, validate, and commit one snapshot under the caller’s deadline.
158
165
  const loadSnapshot = async (
159
166
  request: SecretRequestOptions,
160
- signal: AbortSignal
167
+ signal: AbortSignal,
168
+ environment: string
161
169
  ) => {
162
170
  // Fetch only keys that lack genuine local overrides.
163
171
  const local = readOverrides(keySet, environmentVariables)
@@ -261,6 +269,7 @@ export async function defineSecretSet<
261
269
  cached = next
262
270
  parsed = nextParsed
263
271
  sources = nextSources
272
+ initialized = true
264
273
  }
265
274
 
266
275
  const load = (request: SecretRequestOptions = {}): Promise<void> => {
@@ -273,12 +282,18 @@ export async function defineSecretSet<
273
282
  const start = Date.now()
274
283
 
275
284
  try {
285
+ environment ??= resolveSecretEnvironment(
286
+ options.environment,
287
+ environmentVariables
288
+ )
289
+ const resolvedEnvironment = environment
290
+
276
291
  await withSecretDeadline(
277
292
  {
278
293
  timeoutMs: request.timeoutMs ?? options.timeoutMs,
279
294
  signal: request.signal ?? options.signal
280
295
  },
281
- (signal) => loadSnapshot(request, signal)
296
+ (signal) => loadSnapshot(request, signal, resolvedEnvironment)
282
297
  )
283
298
 
284
299
  // Report the sources of committed values, not attempted fetches.
@@ -293,7 +308,7 @@ export async function defineSecretSet<
293
308
  const counts = Object.fromEntries(sourceCounts)
294
309
 
295
310
  log('info', 'Loaded secrets', {
296
- environment,
311
+ environment: resolvedEnvironment,
297
312
  count: keySet.length,
298
313
  durationMs: Date.now() - start,
299
314
  source: Object.keys(counts).join(' + ') || 'none',
@@ -306,7 +321,7 @@ export async function defineSecretSet<
306
321
  : new SecretLoadError('LOADER', 'Secret loading failed.')
307
322
 
308
323
  log('error', 'Secret loading failed', {
309
- environment,
324
+ environment: environment ?? 'unresolved',
310
325
  count: keySet.length,
311
326
  durationMs: Date.now() - start,
312
327
  code: safe.code
@@ -321,8 +336,19 @@ export async function defineSecretSet<
321
336
  return inFlight
322
337
  }
323
338
 
324
- const secretSet: SecretSet<K, Parsed> = {
339
+ const assertLoaded = () => {
340
+ if (!initialized) {
341
+ throw new SecretLoadError(
342
+ 'CONFIGURATION',
343
+ 'Secret set has not been loaded. Await load() before reading values.'
344
+ )
345
+ }
346
+ }
347
+
348
+ const secretSet: LazySecretSet<K, Parsed> = {
325
349
  secret(key) {
350
+ assertLoaded()
351
+
326
352
  if (!Object.hasOwn(cached, key)) {
327
353
  throw new SecretLoadError('MISSING_SECRETS', `Secret ${key} not found`)
328
354
  }
@@ -331,9 +357,29 @@ export async function defineSecretSet<
331
357
  },
332
358
 
333
359
  // Return copies so consumers cannot mutate the internal snapshots.
334
- secrets: () => ({ ...cached }),
335
- parsed: () => structuredClone(parsed),
336
- sources: () => ({ ...sources }),
360
+ secrets() {
361
+ assertLoaded()
362
+
363
+ return { ...cached }
364
+ },
365
+ parsed() {
366
+ assertLoaded()
367
+
368
+ return structuredClone(parsed)
369
+ },
370
+ sources() {
371
+ assertLoaded()
372
+
373
+ return { ...sources }
374
+ },
375
+
376
+ async load(request) {
377
+ if (!initialized) {
378
+ await load(request)
379
+ }
380
+
381
+ return secretSet
382
+ },
337
383
 
338
384
  async reload(request) {
339
385
  await load(request)
@@ -342,7 +388,17 @@ export async function defineSecretSet<
342
388
  }
343
389
  }
344
390
 
345
- await load()
346
-
347
391
  return secretSet
348
392
  }
393
+
394
+ /** Compatibility API: declares and loads a complete snapshot before returning. */
395
+ export async function defineSecretSet<
396
+ const Keys extends readonly string[],
397
+ Parsed = Record<Keys[number], string>
398
+ >(
399
+ keys: Keys,
400
+ options: CustomLoaderDefineSecretSetOptions<Keys[number], Parsed>,
401
+ environmentVariables?: EnvironmentVariables
402
+ ): Promise<SecretSet<Keys[number], Parsed>> {
403
+ return createSecretSet(keys, options, environmentVariables).load()
404
+ }
@@ -1,5 +1,6 @@
1
1
  import {
2
- defineSecretSet as defineCoreSecretSet,
2
+ createSecretSet as createCoreSecretSet,
3
+ type LazySecretSet,
3
4
  type CommonSecretSetOptions,
4
5
  type CustomLoaderDefineSecretSetOptions,
5
6
  type SecretSet,
@@ -51,13 +52,13 @@ export function getSecretEnvironment(): SecretEnvironment {
51
52
  ) as SecretEnvironment
52
53
  }
53
54
 
54
- export async function defineSecretSet<
55
+ export function createSecretSet<
55
56
  const Keys extends readonly string[],
56
57
  Parsed = Record<Keys[number], string>
57
58
  >(
58
59
  keys: Keys,
59
60
  options: DefineSecretSetOptions<Keys[number], Parsed>
60
- ): Promise<SecretSet<Keys[number], Parsed>> {
61
+ ): LazySecretSet<Keys[number], Parsed> {
61
62
  let loader: SecretSetLoader<Keys[number]>
62
63
 
63
64
  if (!options.loader) {
@@ -69,7 +70,7 @@ export async function defineSecretSet<
69
70
  }
70
71
 
71
72
  // Supply process.env only from this Node/Bun entry point, keeping the core portable.
72
- return defineCoreSecretSet(
73
+ return createCoreSecretSet(
73
74
  keys,
74
75
  { ...options, loader },
75
76
  getProcessEnvironment()
@@ -92,3 +93,14 @@ function createDefaultInfisicalLoader<K extends string, Parsed>(
92
93
  options.clientSecretEnvKey ?? 'INFISICAL_CLIENT_SECRET'
93
94
  })
94
95
  }
96
+
97
+ /** Compatibility API: load immediately and fail before application startup. */
98
+ export async function defineSecretSet<
99
+ const Keys extends readonly string[],
100
+ Parsed = Record<Keys[number], string>
101
+ >(
102
+ keys: Keys,
103
+ options: DefineSecretSetOptions<Keys[number], Parsed>
104
+ ): Promise<SecretSet<Keys[number], Parsed>> {
105
+ return createSecretSet(keys, options).load()
106
+ }