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.
Files changed (55) hide show
  1. package/README.md +70 -4
  2. package/dist/api-wrapper/builders.d.ts +13 -2
  3. package/dist/api-wrapper/errors.d.ts +1 -0
  4. package/dist/api-wrapper/headers.d.ts +2 -1
  5. package/dist/api-wrapper/transport.d.ts +8 -5
  6. package/dist/api-wrapper/types.d.ts +1 -0
  7. package/dist/cache/index.d.ts +26 -0
  8. package/dist/secrets/core.d.ts +47 -0
  9. package/dist/secrets/deadline.d.ts +6 -0
  10. package/dist/secrets/environment.d.ts +9 -0
  11. package/dist/secrets/errors.d.ts +11 -0
  12. package/dist/secrets/index.d.ts +13 -25
  13. package/dist/secrets/infisical-cli.d.ts +6 -0
  14. package/dist/secrets/infisical-config.d.ts +17 -0
  15. package/dist/secrets/infisical-session.d.ts +4 -0
  16. package/dist/secrets/infisical.d.ts +8 -13
  17. package/dist/secrets/loaders.d.ts +5 -12
  18. package/dist/secrets/vite.d.ts +7 -3
  19. package/dist/secrets/worker-loader.d.ts +9 -0
  20. package/dist/secrets/worker.d.ts +3 -0
  21. package/dist/src/api-wrapper/index.js +152 -122
  22. package/dist/src/index.js +3173 -2713
  23. package/dist/src/jwt/index.js +2 -2
  24. package/dist/src/p-queue/index.js +3 -3
  25. package/dist/src/secrets/index.js +670 -402
  26. package/dist/src/secrets/infisical.js +451 -168
  27. package/dist/src/secrets/loaders.js +467 -174
  28. package/dist/src/secrets/vite.js +33 -11
  29. package/dist/src/secrets/worker.js +275 -0
  30. package/dist/src/sitemap/index.js +1 -1
  31. package/dist/src/zod/index.js +7 -7
  32. package/dist/typedswitch/index.d.ts +9 -5
  33. package/package.json +12 -3
  34. package/src/api-wrapper/builders.ts +68 -43
  35. package/src/api-wrapper/create-client.ts +82 -62
  36. package/src/api-wrapper/errors.ts +10 -0
  37. package/src/api-wrapper/headers.ts +14 -1
  38. package/src/api-wrapper/redaction.ts +1 -0
  39. package/src/api-wrapper/transport.ts +34 -38
  40. package/src/api-wrapper/types.ts +1 -0
  41. package/src/cache/index.ts +228 -61
  42. package/src/secrets/core.ts +348 -0
  43. package/src/secrets/deadline.ts +69 -0
  44. package/src/secrets/environment.ts +69 -0
  45. package/src/secrets/errors.ts +29 -0
  46. package/src/secrets/index.ts +79 -210
  47. package/src/secrets/infisical-cli.ts +99 -0
  48. package/src/secrets/infisical-config.ts +70 -0
  49. package/src/secrets/infisical-session.ts +42 -0
  50. package/src/secrets/infisical.ts +214 -66
  51. package/src/secrets/loaders.ts +24 -48
  52. package/src/secrets/vite.ts +44 -15
  53. package/src/secrets/worker-loader.ts +56 -0
  54. package/src/secrets/worker.ts +4 -0
  55. package/src/typedswitch/index.ts +80 -69
@@ -1,15 +1,20 @@
1
1
  import { z } from 'zod'
2
- import { createApiClient } from '../api-wrapper'
3
- import type { SecretEnvironment } from '.'
2
+ import { createApiClient, HttpResponseError } from '../api-wrapper'
3
+ import { withSecretDeadline } from './deadline'
4
+ import { getProcessEnvironment } from './environment'
5
+ import { SecretLoadError } from './errors'
6
+ import { readInfisicalCliToken } from './infisical-cli'
7
+ import { validateInfisicalScope, type InfisicalScope } from './infisical-config'
8
+ import { parseInfisicalSessionToken } from './infisical-session'
4
9
 
5
10
  export const DEFAULT_INFISICAL_SITE_URL = 'https://eu.infisical.com'
6
11
 
7
- const defaultFetch = globalThis.fetch
12
+ export type InfisicalAuthMode = 'auto' | 'http' | 'cli'
8
13
 
9
- const infisicalAuthResponseSchema = z.object({
10
- accessToken: z.string()
11
- })
14
+ const defaultFetch = globalThis.fetch
12
15
 
16
+ // Validate provider responses before exposing any values to a secret set.
17
+ const infisicalAuthResponseSchema = z.object({ accessToken: z.string() })
13
18
  const infisicalSecretSchema = z.object({
14
19
  secretKey: z.string(),
15
20
  secretValue: z.string()
@@ -19,84 +24,204 @@ const infisicalSecretsResponseSchema = z.object({
19
24
  secrets: z.array(infisicalSecretSchema)
20
25
  })
21
26
 
22
- export async function fetchInfisicalSecrets<SecretKey extends string>({
23
- keys,
24
- projectId,
25
- environment,
26
- siteUrl,
27
- clientId,
28
- clientSecret
29
- }: {
30
- readonly keys: readonly SecretKey[]
31
- readonly projectId: string
32
- readonly environment: SecretEnvironment
33
- readonly siteUrl: string
34
- readonly clientId: string
35
- readonly clientSecret: string
36
- }): Promise<Partial<Record<SecretKey, string>>> {
37
- const infisical = createInfisicalApiClient(siteUrl)
38
- const { data: auth } = await infisical.request('login', {
39
- reqBody: {
40
- clientId,
41
- clientSecret
27
+ export async function fetchInfisicalSecrets<K extends string>(
28
+ options: InfisicalScope<K> & {
29
+ readonly clientId: string
30
+ readonly clientSecret: string
31
+ }
32
+ ): Promise<Partial<Record<K, string>>> {
33
+ validateInfisicalScope(options)
34
+
35
+ return withSecretDeadline(options, async (signal) => {
36
+ let operation = 'login'
37
+
38
+ try {
39
+ const infisical = createInfisicalApiClient(options.siteUrl)
40
+ const { data: auth } = await infisical.request('login', {
41
+ signal,
42
+ reqBody: {
43
+ clientId: options.clientId,
44
+ clientSecret: options.clientSecret
45
+ }
46
+ })
47
+
48
+ // Authentication and fetching share a deadline. Do not start a second
49
+ // request if the caller cancelled while authentication was in progress.
50
+ signal.throwIfAborted()
51
+ operation = 'listSecrets'
52
+
53
+ return await listInfisicalSecrets(infisical, auth.accessToken, {
54
+ ...options,
55
+ signal
56
+ })
57
+ } catch (error) {
58
+ throwInfisicalHttpError(error, operation, signal)
42
59
  }
43
60
  })
61
+ }
62
+
63
+ export type LoadInfisicalSecretsFromEnvOptions<K extends string> =
64
+ InfisicalScope<K> & {
65
+ readonly clientIdEnvKey: string
66
+ readonly clientSecretEnvKey: string
67
+ readonly auth?: InfisicalAuthMode
68
+ readonly reportSource?: (source: string) => void
69
+ }
70
+
71
+ export async function loadInfisicalSecrets<K extends string>(
72
+ options: LoadInfisicalSecretsFromEnvOptions<K>
73
+ ): Promise<Partial<Record<K, string>>> {
74
+ validateInfisicalScope(options)
75
+
76
+ const env = getProcessEnvironment()
77
+ const clientId = env?.[options.clientIdEnvKey]
78
+ const clientSecret = env?.[options.clientSecretEnvKey]
79
+ const mode = options.auth ?? 'auto'
80
+
81
+ if (!['auto', 'http', 'cli'].includes(mode)) {
82
+ throw new SecretLoadError('CONFIGURATION', 'Invalid Infisical auth mode.')
83
+ }
84
+
85
+ // Explicit modes take precedence. Auto mode uses HTTP only when both
86
+ // credentials exist; a failed HTTP request must not switch accounts.
87
+ const useHttp =
88
+ mode === 'http' || (mode === 'auto' && Boolean(clientId && clientSecret))
89
+
90
+ if (useHttp) {
91
+ if (!clientId || !clientSecret) {
92
+ throw new SecretLoadError(
93
+ 'AUTHENTICATION',
94
+ 'Infisical HTTP authentication requires both client credentials.'
95
+ )
96
+ }
97
+
98
+ options.reportSource?.('Infisical HTTP')
99
+
100
+ return fetchInfisicalSecrets({ ...options, clientId, clientSecret })
101
+ }
102
+
103
+ options.reportSource?.(
104
+ `Infisical CLI session (organization=${options.organizationId} project=${options.projectId} environment=${options.environment})`
105
+ )
106
+
107
+ return withSecretDeadline(options, (signal) =>
108
+ fetchInfisicalSessionSecrets({ ...options, signal })
109
+ )
110
+ }
111
+
112
+ async function fetchInfisicalSessionSecrets<K extends string>(
113
+ options: InfisicalScope<K> & { signal: AbortSignal }
114
+ ): Promise<Partial<Record<K, string>>> {
115
+ const { signal } = options
116
+ let token = await readInfisicalCliToken(options)
117
+
118
+ signal.throwIfAborted()
119
+
120
+ const session = parseInfisicalSessionToken(token)
121
+ const infisical = createInfisicalApiClient(options.siteUrl)
122
+ let operation = 'selectOrganization'
123
+
124
+ try {
125
+ // JWT claims only guide routing; Infisical verifies authentication and access.
126
+ // An already scoped session needs no exchange (or additional MFA challenge).
127
+ if (session.organizationId !== options.organizationId) {
128
+ const { data } = await infisical.request('selectOrganization', {
129
+ signal,
130
+ reqHeaders: {
131
+ Authorization: `Bearer ${token}`,
132
+ 'User-Agent': 'hanzio/secrets'
133
+ },
134
+ reqBody: { organizationId: options.organizationId }
135
+ })
136
+
137
+ signal.throwIfAborted()
138
+
139
+ if (data.isMfaEnabled) {
140
+ throw new SecretLoadError(
141
+ 'AUTHENTICATION',
142
+ 'Infisical requires MFA for this organization. Complete infisical login for the target organization and configured domain before loading secrets.',
143
+ { operation }
144
+ )
145
+ }
146
+
147
+ const scopedSession = parseInfisicalSessionToken(data.token)
148
+
149
+ if (scopedSession.organizationId !== options.organizationId) {
150
+ throw new SecretLoadError(
151
+ 'AUTHENTICATION',
152
+ 'Infisical returned a session for a different organization.',
153
+ { operation }
154
+ )
155
+ }
156
+
157
+ // Keep the exchanged token local: concurrent sets must never share or
158
+ // overwrite the CLI's persisted session or another organization's token.
159
+ token = data.token
160
+ }
161
+
162
+ signal.throwIfAborted()
163
+ operation = 'listSecrets'
164
+
165
+ return await listInfisicalSecrets(infisical, token, options)
166
+ } catch (error) {
167
+ throwInfisicalHttpError(error, operation, signal)
168
+ }
169
+ }
170
+
171
+ async function listInfisicalSecrets<K extends string>(
172
+ infisical: ReturnType<typeof createInfisicalApiClient>,
173
+ token: string,
174
+ options: InfisicalScope<K> & { signal: AbortSignal }
175
+ ): Promise<Partial<Record<K, string>>> {
44
176
  const { data } = await infisical.request('listSecrets', {
45
- reqHeaders: {
46
- Authorization: `Bearer ${auth.accessToken}`
47
- },
177
+ signal: options.signal,
178
+ reqHeaders: { Authorization: `Bearer ${token}` },
48
179
  reqQuery: {
49
- environment,
50
- projectId,
51
- secretPath: '/',
180
+ environment: options.environment,
181
+ projectId: options.projectId,
182
+ secretPath: options.secretPath ?? '/',
52
183
  viewSecretValue: true,
53
184
  expandSecretReferences: true,
54
185
  includeImports: true
55
186
  }
56
187
  })
57
188
 
58
- const wantedKeys = new Set<string>(keys)
189
+ // Infisical lists the folder, but callers only receive their requested keys.
190
+ const wantedKeys = new Set<string>(options.keys)
191
+
59
192
  return Object.fromEntries(
60
193
  data.secrets
61
194
  .filter((secret) => wantedKeys.has(secret.secretKey))
62
195
  .map((secret) => [secret.secretKey, secret.secretValue])
63
- ) as Partial<Record<SecretKey, string>>
196
+ ) as Partial<Record<K, string>>
64
197
  }
65
198
 
66
- export type LoadInfisicalSecretsFromEnvOptions<SecretKey extends string> = {
67
- readonly keys: readonly SecretKey[]
68
- readonly projectId: string
69
- readonly environment: SecretEnvironment
70
- readonly siteUrl: string
71
- readonly clientIdEnvKey: string
72
- readonly clientSecretEnvKey: string
73
- }
199
+ function throwInfisicalHttpError(
200
+ error: unknown,
201
+ operation: string,
202
+ signal: AbortSignal
203
+ ): never {
204
+ if (signal.aborted) {
205
+ throw signal.reason
206
+ }
74
207
 
75
- export async function loadInfisicalSecrets<SecretKey extends string>({
76
- keys,
77
- projectId,
78
- environment,
79
- siteUrl,
80
- clientIdEnvKey,
81
- clientSecretEnvKey
82
- }: LoadInfisicalSecretsFromEnvOptions<SecretKey>): Promise<
83
- Partial<Record<SecretKey, string>>
84
- > {
85
- const clientId = process.env[clientIdEnvKey]
86
- const clientSecret = process.env[clientSecretEnvKey]
87
-
88
- if (!clientId || !clientSecret) {
89
- throw new Error('Missing Infisical credentials')
208
+ if (error instanceof SecretLoadError) {
209
+ throw error
90
210
  }
91
211
 
92
- return fetchInfisicalSecrets({
93
- keys,
94
- projectId,
95
- environment,
96
- siteUrl,
97
- clientId,
98
- clientSecret
99
- })
212
+ // API-wrapper errors can retain request bodies and raw responses.
213
+ // Copy safe metadata only; never preserve the original error as a cause.
214
+ const status = error instanceof HttpResponseError ? error.status : undefined
215
+ const guidance =
216
+ operation === 'selectOrganization' && (status === 401 || status === 403)
217
+ ? ' Run infisical login for the target organization and configured domain, then verify your access.'
218
+ : ''
219
+
220
+ throw new SecretLoadError(
221
+ 'HTTP',
222
+ `Infisical HTTP ${operation} failed${status ? ` (status ${status})` : ''}.${guidance}`,
223
+ { operation, status }
224
+ )
100
225
  }
101
226
 
102
227
  function createInfisicalApiClient(siteUrl: string) {
@@ -109,6 +234,22 @@ function createInfisicalApiClient(siteUrl: string) {
109
234
  fetch: getFetch(),
110
235
  retries: 1,
111
236
  endpoints: {
237
+ // Internal endpoint also used by Infisical CLI init. Keep its contract
238
+ // isolated here so upstream authentication changes have one integration point.
239
+ selectOrganization: {
240
+ method: 'POST',
241
+ path: '/api/v3/auth/select-organization',
242
+ reqHeadersSchema: z.object({
243
+ Authorization: z.string(),
244
+ 'User-Agent': z.string()
245
+ }),
246
+ reqBodySchema: z.object({ organizationId: z.string() }),
247
+ resSchema: z.object({
248
+ token: z.string().min(1),
249
+ isMfaEnabled: z.boolean().optional()
250
+ }),
251
+ reqBodyFormat: 'json'
252
+ },
112
253
  login: {
113
254
  method: 'POST',
114
255
  path: '/api/v1/auth/universal-auth/login',
@@ -140,5 +281,12 @@ function createInfisicalApiClient(siteUrl: string) {
140
281
  }
141
282
 
142
283
  function getFetch(): typeof fetch {
143
- return globalThis.fetch ?? defaultFetch
284
+ const fetchFn = globalThis.fetch ?? defaultFetch
285
+
286
+ // A retry may wake after the overall deadline; stop before issuing fetch.
287
+ return ((input, init) => {
288
+ init?.signal?.throwIfAborted()
289
+
290
+ return fetchFn(input, init)
291
+ }) as typeof fetch
144
292
  }
@@ -1,67 +1,43 @@
1
1
  import { DEFAULT_INFISICAL_SITE_URL, fetchInfisicalSecrets } from './infisical'
2
- import type { SecretSetLoader, SecretSetLoaderContext } from './index'
2
+ import {
3
+ validateInfisicalOrganizationId,
4
+ type InfisicalProjectOptions
5
+ } from './infisical-config'
6
+ import type { SecretSetLoader, SecretSetLoaderContext } from './core'
3
7
 
4
- /**
5
- * Reads secrets from a Cloudflare Worker `env` bindings object (or any similar
6
- * `Record`), for Workers where `process.env` is not populated. Pass the same
7
- * object you get as `env` in your fetch handler, or values you’d read from
8
- * `import { env } from "cloudflare:workers"` in your Worker bundle — this
9
- * package does not import that module.
10
- */
11
- export function cloudflareWorkerEnvLoader<SecretKey extends string>(
12
- workerEnv: Record<string, unknown>
13
- ): SecretSetLoader<SecretKey> {
14
- return async (ctx) => {
15
- const out: Partial<Record<SecretKey, string>> = {}
16
- for (const key of ctx.keys) {
17
- const str = workerBindingToString(workerEnv[key])
18
- if (str !== undefined) {
19
- out[key] = str
20
- }
21
- }
22
- return out
23
- }
24
- }
8
+ export { cloudflareWorkerEnvLoader } from './worker-loader'
25
9
 
26
- function workerBindingToString(value: unknown): string | undefined {
27
- if (value === undefined || value === null) {
28
- return undefined
29
- }
30
- if (typeof value === 'string') {
31
- return value === '' ? undefined : value
32
- }
33
- if (
34
- typeof value === 'number' ||
35
- typeof value === 'boolean' ||
36
- typeof value === 'bigint'
37
- ) {
38
- return String(value)
10
+ export function infisicalLoader<SecretKey extends string>(
11
+ config: InfisicalProjectOptions & {
12
+ readonly clientId: string
13
+ readonly clientSecret: string
14
+ readonly timeoutMs?: number
39
15
  }
40
- if (typeof value === 'object') {
41
- return JSON.stringify(value)
42
- }
43
- return String(value)
44
- }
16
+ ): SecretSetLoader<SecretKey> {
17
+ validateInfisicalOrganizationId(config.organizationId)
45
18
 
46
- export function infisicalLoader<SecretKey extends string>(config: {
47
- readonly projectId: string
48
- readonly clientId: string
49
- readonly clientSecret: string
50
- readonly siteUrl?: string
51
- }): SecretSetLoader<SecretKey> {
52
- return async (ctx) =>
19
+ const loader: SecretSetLoader<SecretKey> = async (ctx) =>
53
20
  fetchInfisicalSecrets({
54
21
  keys: ctx.keys,
55
22
  projectId: config.projectId,
23
+ organizationId: config.organizationId,
56
24
  environment: ctx.environment,
57
25
  siteUrl: config.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
58
26
  clientId: config.clientId,
59
- clientSecret: config.clientSecret
27
+ clientSecret: config.clientSecret,
28
+ secretPath: config.secretPath,
29
+ timeoutMs: config.timeoutMs ?? ctx.timeoutMs,
30
+ signal: ctx.signal
60
31
  })
32
+
33
+ return Object.assign(loader, { source: 'Infisical HTTP' })
61
34
  }
62
35
 
63
36
  export async function processEnvLoader<SecretKey extends string>(
64
37
  _ctx: SecretSetLoaderContext<SecretKey>
65
38
  ): Promise<Partial<Record<SecretKey, string>>> {
39
+ // The core has already read environment overrides; there is no remote source.
66
40
  return {}
67
41
  }
42
+
43
+ Object.assign(processEnvLoader, { source: 'process.env' })
@@ -1,30 +1,59 @@
1
- import type { SecretSet } from '.'
1
+ import type { SecretSet } from './core'
2
+ import { SecretLoadError } from './errors'
2
3
 
3
4
  export type ViteSecretSetPlugin = {
4
5
  name: string
5
- config: () => {
6
- define: Record<string, string>
7
- }
6
+ config: () => { define: Record<string, string> }
7
+ }
8
+
9
+ export type ViteSecretSetOptions<K extends string> = {
10
+ /** Explicit acknowledgement that these values may be shipped to browsers. */
11
+ readonly publicKeys?: readonly K[]
8
12
  }
9
13
 
10
- export function getViteDefine<SecretKey extends string>(
11
- secretSet: SecretSet<SecretKey>
14
+ export function getViteDefine<K extends string>(
15
+ secretSet: Pick<SecretSet<K, unknown>, 'secrets'>,
16
+ options: ViteSecretSetOptions<K> = {}
12
17
  ): Record<string, string> {
18
+ const values = secretSet.secrets()
19
+ const keys = options.publicKeys ?? (Object.keys(values) as K[])
20
+
21
+ // An explicit allowlist is required before exposing non-prefixed values.
22
+ if (!options.publicKeys && keys.some((key) => !key.startsWith('VITE_'))) {
23
+ throw new SecretLoadError(
24
+ 'CONFIGURATION',
25
+ 'Vite secret sets must contain only VITE_ keys, or provide an explicit publicKeys list.'
26
+ )
27
+ }
28
+
29
+ for (const key of keys) {
30
+ if (!Object.hasOwn(values, key) || !/^[A-Za-z_$][\w$]*$/.test(key)) {
31
+ throw new SecretLoadError(
32
+ 'CONFIGURATION',
33
+ 'Vite publicKeys must reference configured keys with valid JavaScript identifiers.'
34
+ )
35
+ }
36
+
37
+ if (['MODE', 'BASE_URL', 'PROD', 'DEV', 'SSR'].includes(key)) {
38
+ throw new SecretLoadError(
39
+ 'CONFIGURATION',
40
+ 'Vite built-in environment constants cannot be overridden by secrets.'
41
+ )
42
+ }
43
+ }
44
+
45
+ // Only selected public values become browser-visible replacements.
13
46
  return Object.fromEntries(
14
- Object.entries(secretSet.secrets()).map(([key, value]) => [
15
- `import.meta.env.${key}`,
16
- JSON.stringify(value)
17
- ])
47
+ keys.map((key) => [`import.meta.env.${key}`, JSON.stringify(values[key])])
18
48
  )
19
49
  }
20
50
 
21
- export function viteSecretSetPlugin<SecretKey extends string>(
22
- secretSet: SecretSet<SecretKey>
51
+ export function viteSecretSetPlugin<K extends string>(
52
+ secretSet: Pick<SecretSet<K, unknown>, 'secrets'>,
53
+ options: ViteSecretSetOptions<K> = {}
23
54
  ): ViteSecretSetPlugin {
24
55
  return {
25
56
  name: 'hanzio-secret-set',
26
- config: () => ({
27
- define: getViteDefine(secretSet)
28
- })
57
+ config: () => ({ define: getViteDefine(secretSet, options) })
29
58
  }
30
59
  }
@@ -0,0 +1,56 @@
1
+ import type { SecretSetLoader } from './core'
2
+
3
+ /**
4
+ * Reads secrets from a Cloudflare Worker `env` bindings object (or any similar
5
+ * `Record`), for Workers where `process.env` is not populated. Pass the same
6
+ * object you get as `env` in your fetch handler, or values you’d read from
7
+ * `import { env } from "cloudflare:workers"` in your Worker bundle — this
8
+ * package does not import that module.
9
+ */
10
+ export function cloudflareWorkerEnvLoader<SecretKey extends string>(
11
+ workerEnv: Record<string, unknown>
12
+ ): SecretSetLoader<SecretKey> {
13
+ const loader: SecretSetLoader<SecretKey> = async (ctx) => {
14
+ // A null prototype lets secret names safely overlap with object properties.
15
+ const out: Partial<Record<SecretKey, string>> = Object.create(null)
16
+
17
+ for (const key of ctx.keys) {
18
+ const str = workerBindingToString(
19
+ Object.hasOwn(workerEnv, key) ? workerEnv[key] : undefined
20
+ )
21
+
22
+ if (str !== undefined) {
23
+ out[key] = str
24
+ }
25
+ }
26
+
27
+ return out
28
+ }
29
+
30
+ return Object.assign(loader, { source: 'Cloudflare Worker bindings' })
31
+ }
32
+
33
+ function workerBindingToString(value: unknown): string | undefined {
34
+ if (value === undefined || value === null) {
35
+ return undefined
36
+ }
37
+
38
+ if (typeof value === 'string') {
39
+ return value === '' ? undefined : value
40
+ }
41
+
42
+ if (
43
+ typeof value === 'number' ||
44
+ typeof value === 'boolean' ||
45
+ typeof value === 'bigint'
46
+ ) {
47
+ return String(value)
48
+ }
49
+
50
+ // JSON bindings remain strings until the optional schema parses them.
51
+ if (typeof value === 'object') {
52
+ return JSON.stringify(value)
53
+ }
54
+
55
+ return String(value)
56
+ }
@@ -0,0 +1,4 @@
1
+ // Runtime-neutral entry point: no process globals or Infisical CLI dependencies.
2
+ export * from './core'
3
+ export { cloudflareWorkerEnvLoader } from './worker-loader'
4
+ export type { SecretRequestOptions } from './deadline'