hanzio 2.0.0 → 2.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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -6
  3. package/dist/api-wrapper/endpoint.d.ts +8 -5
  4. package/dist/api-wrapper/transport.d.ts +1 -0
  5. package/dist/api-wrapper/types.d.ts +7 -0
  6. package/dist/{index-wjyb9bsd.js → index-hyt6c56h.js} +39 -12
  7. package/dist/index-ktynw3vs.js +37 -0
  8. package/dist/{index-181e6vfw.js → index-w882zry3.js} +1 -1
  9. package/dist/{index-j1ybktbr.js → index-zbj9s3s3.js} +6 -4
  10. package/dist/secrets/core.d.ts +7 -0
  11. package/dist/secrets/index.d.ts +3 -1
  12. package/dist/secrets/public-config.d.ts +7 -0
  13. package/dist/secrets/vite.d.ts +1 -0
  14. package/dist/secrets/worker.d.ts +1 -0
  15. package/dist/src/api-wrapper/index.js +1 -1
  16. package/dist/src/secrets/index.js +14 -8
  17. package/dist/src/secrets/vite.js +5 -1
  18. package/dist/src/secrets/worker.js +8 -4
  19. package/package.json +10 -3
  20. package/src/api-wrapper/README.md +738 -0
  21. package/src/api-wrapper/create-client.ts +9 -3
  22. package/src/api-wrapper/endpoint.ts +19 -5
  23. package/src/api-wrapper/transport.ts +6 -2
  24. package/src/api-wrapper/types.ts +13 -0
  25. package/src/crypto/README.md +144 -0
  26. package/src/format/README.md +120 -0
  27. package/src/i18n/README.md +65 -0
  28. package/src/jwt/README.md +70 -0
  29. package/src/p-queue/README.md +237 -0
  30. package/src/secrets/README.md +413 -0
  31. package/src/secrets/core.ts +72 -16
  32. package/src/secrets/index.ts +16 -4
  33. package/src/secrets/public-config.ts +47 -0
  34. package/src/secrets/vite.ts +2 -0
  35. package/src/secrets/worker.ts +2 -0
  36. package/src/sitemap/README.md +61 -0
  37. package/src/storage/README.md +71 -0
  38. package/src/typedswitch/README.md +319 -0
  39. package/dist/index-srt06xd2.js +0 -15
@@ -0,0 +1,237 @@
1
+ ---
2
+ description: Promise queue with concurrency, priority, timeout, cancellation, and rate-limit controls.
3
+ ---
4
+
5
+ # hanzio/p-queue
6
+
7
+ `hanzio/p-queue` is a small dependency-free promise queue inspired by
8
+ `p-queue`. Use it when you need to run async work with bounded concurrency,
9
+ priority ordering, basic rate limiting, or queue lifecycle hooks.
10
+
11
+ ```ts
12
+ import PQueue from 'hanzio/p-queue'
13
+
14
+ const queue = new PQueue({ concurrency: 2 })
15
+
16
+ const results = await Promise.all([
17
+ queue.add(async () => fetchUser('1')),
18
+ queue.add(async () => fetchUser('2')),
19
+ queue.add(async () => fetchUser('3'))
20
+ ])
21
+
22
+ await queue.onIdle()
23
+ ```
24
+
25
+ ## Common Usage
26
+
27
+ ### Limit Concurrency
28
+
29
+ ```ts
30
+ const queue = new PQueue({ concurrency: 3 })
31
+
32
+ for (const id of userIds) {
33
+ queue.add(async () => syncUser(id))
34
+ }
35
+
36
+ await queue.onIdle()
37
+ ```
38
+
39
+ Only three tasks run at the same time. Queued tasks start as running tasks
40
+ finish.
41
+
42
+ ### Add Multiple Tasks
43
+
44
+ ```ts
45
+ const results = await queue.addAll([
46
+ async () => 'first',
47
+ async () => 'second'
48
+ ])
49
+ ```
50
+
51
+ `addAll` resolves results in the same order as the input functions.
52
+
53
+ ### Pause And Resume
54
+
55
+ ```ts
56
+ const queue = new PQueue({ autoStart: false })
57
+
58
+ queue.add(async () => syncUser('1'))
59
+ queue.add(async () => syncUser('2'))
60
+
61
+ queue.start()
62
+ await queue.onIdle()
63
+ ```
64
+
65
+ Use `pause()` to stop starting new queued tasks. Running tasks continue.
66
+
67
+ ```ts
68
+ queue.pause()
69
+ queue.start()
70
+ ```
71
+
72
+ ### Priority
73
+
74
+ Higher priority tasks run first. Tasks with the same priority keep FIFO order.
75
+
76
+ ```ts
77
+ const queue = new PQueue({ concurrency: 1, autoStart: false })
78
+
79
+ queue.add(async () => 'low', { priority: 0, id: 'low' })
80
+ queue.add(async () => 'high', { priority: 10, id: 'high' })
81
+
82
+ queue.start()
83
+ ```
84
+
85
+ You can update queued task priority by id before it starts:
86
+
87
+ ```ts
88
+ queue.setPriority('low', 20)
89
+ ```
90
+
91
+ ### Timeout
92
+
93
+ Timeouts begin when the task starts running, not while it is waiting in the
94
+ queue.
95
+
96
+ ```ts
97
+ import PQueue, { TimeoutError } from 'hanzio/p-queue'
98
+
99
+ const queue = new PQueue({ timeout: 1000 })
100
+
101
+ try {
102
+ await queue.add(async () => slowTask())
103
+ } catch (error) {
104
+ if (error instanceof TimeoutError) {
105
+ console.log('Task timed out')
106
+ }
107
+ }
108
+ ```
109
+
110
+ Override the timeout per task:
111
+
112
+ ```ts
113
+ await queue.add(async () => slowTask(), { timeout: 5000 })
114
+ ```
115
+
116
+ ### Cancellation
117
+
118
+ Pass an `AbortSignal` to remove a queued task or reject a running task. The task
119
+ also receives the same signal.
120
+
121
+ ```ts
122
+ const controller = new AbortController()
123
+
124
+ const promise = queue.add(
125
+ async ({ signal }) => {
126
+ const response = await fetch(url, { signal })
127
+ return response.json()
128
+ },
129
+ { signal: controller.signal }
130
+ )
131
+
132
+ controller.abort(new Error('Cancelled'))
133
+ await promise
134
+ ```
135
+
136
+ ### Rate Limiting
137
+
138
+ Use `intervalCap` and `interval` to limit how many tasks start during a time
139
+ window.
140
+
141
+ ```ts
142
+ const queue = new PQueue({
143
+ concurrency: 5,
144
+ intervalCap: 10,
145
+ interval: 1000
146
+ })
147
+ ```
148
+
149
+ Enable `strict` to use a sliding window instead of a fixed window.
150
+
151
+ ```ts
152
+ const queue = new PQueue({
153
+ intervalCap: 10,
154
+ interval: 1000,
155
+ strict: true
156
+ })
157
+ ```
158
+
159
+ ## Waiting For Queue State
160
+
161
+ ```ts
162
+ await queue.onEmpty()
163
+ await queue.onIdle()
164
+ await queue.onPendingZero()
165
+ await queue.onSizeLessThan(10)
166
+ await queue.onRateLimit()
167
+ await queue.onRateLimitCleared()
168
+ ```
169
+
170
+ - `onEmpty()` resolves when there are no queued tasks waiting to start.
171
+ - `onIdle()` resolves when there are no queued or running tasks.
172
+ - `onPendingZero()` resolves when all currently running tasks finish.
173
+ - `onSizeLessThan(limit)` resolves when `queue.size < limit`.
174
+ - `onRateLimit()` resolves when the queue becomes rate-limited.
175
+ - `onRateLimitCleared()` resolves when rate limiting clears.
176
+
177
+ ## Events
178
+
179
+ ```ts
180
+ queue.on('active', () => {
181
+ console.log('A task started')
182
+ })
183
+
184
+ queue.on('completed', (result) => {
185
+ console.log('Task completed', result)
186
+ })
187
+
188
+ queue.on('error', (error) => {
189
+ console.error('Task failed', error)
190
+ })
191
+ ```
192
+
193
+ Supported events:
194
+
195
+ - `add`
196
+ - `active`
197
+ - `completed`
198
+ - `error`
199
+ - `empty`
200
+ - `idle`
201
+ - `pendingZero`
202
+ - `next`
203
+ - `rateLimit`
204
+ - `rateLimitCleared`
205
+
206
+ Use `off(event, listener)` to remove a listener.
207
+
208
+ ## State
209
+
210
+ ```ts
211
+ queue.size
212
+ queue.pending
213
+ queue.isPaused
214
+ queue.isRateLimited
215
+ queue.isSaturated
216
+ queue.runningTasks
217
+ queue.concurrency = 5
218
+ queue.timeout = 2000
219
+ ```
220
+
221
+ - `size` is the number of queued tasks waiting to start.
222
+ - `pending` is the number of running tasks.
223
+ - `runningTasks` returns task metadata for currently running tasks.
224
+ - `clear()` removes queued tasks but does not cancel tasks already running.
225
+
226
+ ## Exports
227
+
228
+ ```ts
229
+ import PQueue, {
230
+ PriorityQueue,
231
+ TimeoutError,
232
+ type Options,
233
+ type Queue,
234
+ type QueueAddOptions,
235
+ type TaskOptions
236
+ } from 'hanzio/p-queue'
237
+ ```
@@ -0,0 +1,413 @@
1
+ ---
2
+ description: Typed secret sets with Infisical HTTP/CLI loading, validation, and runtime isolation.
3
+ ---
4
+
5
+ # hanzio secrets
6
+
7
+ Define required keys and await a complete, validated snapshot before starting
8
+ an application. `secret()` and `secrets()` return strings; missing or empty
9
+ values fail startup.
10
+
11
+ ```ts
12
+ import { defineSecretSet } from 'hanzio/secrets'
13
+
14
+ const secrets = await defineSecretSet(['DATABASE_URL', 'APP_SECRET'], {
15
+ organizationId: 'infisical-organization-id',
16
+ projectId: 'infisical-project-id',
17
+ siteUrl: 'https://eu.infisical.com',
18
+ secretPath: '/backend'
19
+ })
20
+
21
+ const databaseUrl = secrets.secret('DATABASE_URL')
22
+ ```
23
+
24
+ ## Import-safe declarations and public build configuration
25
+
26
+ Use `createSecretSet` when a module is imported by typechecking, build tooling,
27
+ or test runners. Declaration does not run a loader, evaluate the environment
28
+ callback, authenticate, or write environment values. Await `load()` at the
29
+ application entry point before starting servers or workers. Reading before a
30
+ successful load throws a clear configuration error.
31
+
32
+ ```ts
33
+ // backend/config.ts — safe for tooling to import
34
+ import { createSecretSet } from 'hanzio/secrets'
35
+
36
+ export const secrets = createSecretSet(['DATABASE_URL', 'APP_SECRET'], {
37
+ organizationId: 'infisical-organization-id',
38
+ projectId: 'infisical-project-id',
39
+ secretPath: '/backend'
40
+ })
41
+
42
+ // backend/start.ts
43
+ await secrets.load()
44
+ // Start the application only after validation succeeds.
45
+ ```
46
+
47
+ `load()` is idempotent after successful initialization, shares concurrent loads,
48
+ and can retry a failed initialization. `reload()` always refreshes, retaining the
49
+ last successful snapshot on failure. The existing `defineSecretSet` API remains
50
+ an eager declaration plus load, with the same startup validation and overrides.
51
+ The runtime-neutral `hanzio/secrets/worker` entry exports both APIs too.
52
+
53
+ Frontend builds can read public values without a secret provider:
54
+
55
+ ```ts
56
+ import { readPublicConfig, getViteDefine } from 'hanzio/secrets/vite'
57
+
58
+ const publicConfig = readPublicConfig(
59
+ ['VITE_API_URL', 'VITE_FIREBASE_API_KEY'],
60
+ process.env
61
+ )
62
+
63
+ export default { define: getViteDefine(publicConfig) }
64
+ ```
65
+
66
+ `readPublicConfig(keys, values)` is synchronous and runtime-neutral. The caller
67
+ supplies the values explicitly: production build inputs or deterministic CI
68
+ fixtures. It copies only the listed keys, never reads unrelated values, and
69
+ fails with their names if required values are missing, empty, or not strings.
70
+ It never fetches secrets, invents defaults, or writes `.env` / `process.env`.
71
+ Apply your own schema to `publicConfig.secrets()` to validate URLs and other
72
+ formats. The Vite allowlist policy still applies: non-`VITE_` keys require
73
+ `getViteDefine(publicConfig, { publicKeys: [...] })`.
74
+
75
+ Browser Firebase settings, public URLs, Turnstile site keys and PostHog browser
76
+ project keys are public build values. Server credentials and source-map upload
77
+ tokens belong in server or authenticated upload steps, outside this key list.
78
+
79
+ ## Authentication and scope
80
+
81
+ `auth` supports three modes:
82
+
83
+ | Mode | Behavior |
84
+ | --- | --- |
85
+ | `auto` (default) | Uses HTTP when both client credentials are nonempty; otherwise uses the signed-in CLI session. |
86
+ | `http` | Requires both client credentials when remote loading is needed; never falls back to the CLI. |
87
+ | `cli` | Uses the signed-in CLI session even when client credentials exist. |
88
+
89
+ Credentials come from `INFISICAL_CLIENT_ID` and `INFISICAL_CLIENT_SECRET`.
90
+ Override those names with `clientIdEnvKey` and `clientSecretEnvKey`. HTTP
91
+ failures never switch to another authentication mode. If all required keys
92
+ are already supplied by genuine environment overrides, authentication is skipped.
93
+
94
+ For local development, install the [Infisical CLI](https://infisical.com/docs/cli/overview),
95
+ ensure `infisical` is on your application's `PATH`, and sign in:
96
+
97
+ ```sh
98
+ infisical login --domain=https://eu.infisical.com
99
+ ```
100
+
101
+ Use your configured `siteUrl` for login. The default is
102
+ `https://eu.infisical.com`. The helper passes project, environment, domain,
103
+ and `secretPath` (default `/`) explicitly, so `.infisical.json` is unnecessary.
104
+ Both authentication modes fetch secrets over HTTP, including imports and
105
+ expanded secret references.
106
+
107
+ The child environment omits `INFISICAL_TOKEN`,
108
+ `INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN`, and legacy `TOKEN` so they do not
109
+ replace the signed-in user session. The parent environment is unchanged by
110
+ this filtering. CLI output stays in memory and is limited to 10 MiB per stream.
111
+ The helper closes stdin and never executes a shell command or launches login
112
+ itself. Some CLI versions may attempt browser login while reading the token
113
+ if the session expires. Sign in before starting the app.
114
+
115
+ ### Multiple organizations and projects
116
+
117
+ Set `organizationId` alongside `projectId` to use the same signed-in user across
118
+ organizations on the configured Infisical instance:
119
+
120
+ ```ts
121
+ const [first, second] = await Promise.all([
122
+ defineSecretSet(['DATABASE_URL'], {
123
+ organizationId: 'organization-a',
124
+ projectId: 'project-a',
125
+ auth: 'cli',
126
+ writeToProcessEnv: false
127
+ }),
128
+ defineSecretSet(['DATABASE_URL'], {
129
+ organizationId: 'organization-b',
130
+ projectId: 'project-b',
131
+ auth: 'cli',
132
+ writeToProcessEnv: false
133
+ })
134
+ ])
135
+ ```
136
+
137
+ CLI authentication runs `infisical user get token --plain`
138
+ to read the active user's session. If its organization differs, the helper
139
+ exchanges it through `/api/v3/auth/select-organization`, then fetches secrets
140
+ over HTTP. Already matching sessions skip the exchange. Tokens stay in memory
141
+ for that operation; the helper does not persist the exchanged token, switch
142
+ the active CLI organization, or cache tokens between sets or reloads.
143
+
144
+ Organization selection uses the same internal endpoint as
145
+ [Infisical CLI init](https://github.com/Infisical/cli/blob/main/packages/cmd/init.go).
146
+ The adapter may need updating if that contract changes. It honors organization
147
+ access restrictions; MFA challenges, expired sessions, and denied access fail
148
+ with sanitized errors. Complete login for the target organization and domain
149
+ when additional MFA or SSO is required. The helper cannot satisfy interactive
150
+ authentication challenges. The CLI itself may attempt login when its session
151
+ is missing or expired, including during `user get token`.
152
+
153
+ Sign in to the same instance as `siteUrl`: reading a token uses the CLI's active
154
+ profile, and does not automatically choose a different account or instance.
155
+ This HTTP flow requires network access and does not use the CLI's offline
156
+ secret backup. Project, environment, folder, imports, and reference expansion
157
+ use the same HTTP loading options as machine authentication.
158
+
159
+ `organizationId` is required alongside `projectId` in all Infisical options,
160
+ including explicit HTTP loaders. Missing, empty, or whitespace-containing IDs
161
+ fail configuration validation, even when environment overrides supply every
162
+ secret. Custom loaders do not require Infisical configuration.
163
+ With machine credentials (`auth: 'http'`, or `auto` with both credentials), the
164
+ identity's existing organization and permissions apply; `organizationId` does
165
+ not switch a machine identity into another organization. Use `auth: 'cli'` to
166
+ force user authentication locally when machine credentials are also present.
167
+
168
+ Keep `writeToProcessEnv: false` when loading multiple organizations in one
169
+ process. Genuine environment overrides still apply to all sets using that key.
170
+
171
+ For deployments, use `auth: 'http'` and provide machine identity credentials.
172
+ The CLI requires Node.js/Bun subprocess support; it is not available in Workers.
173
+
174
+ ## Environments
175
+
176
+ Selection order is the explicit `environment` option, `SECRETS_ENV`, then
177
+ `NODE_ENV`. An explicitly configured invalid environment fails instead of
178
+ silently selecting development.
179
+
180
+ - `dev` / `development` resolve to `dev`.
181
+ - `staging` resolves to `staging`.
182
+ - `prod` / `production` resolve to `prod`.
183
+ - An absent `SECRETS_ENV` uses the usual `NODE_ENV` mapping; unset, test, and
184
+ other `NODE_ENV` values default to `dev`.
185
+ - A custom Infisical environment must be explicit: `environment: { slug: 'preview-123' }`.
186
+
187
+ `environment` also accepts a function returning a standard environment or
188
+ `{ slug }`. It is resolved on the first load (`defineSecretSet` loads immediately); reload keeps that scope.
189
+ An empty `SECRETS_ENV` is invalid—unset it to use the default.
190
+
191
+ ## Overrides, isolation, and reload
192
+
193
+ Genuine `process.env` overrides take precedence. By default, fetched values
194
+ are also written to `process.env` for compatibility with existing applications.
195
+ The helper tracks its own writes: a second set cannot mistake another set's
196
+ fetched values for user overrides. Original local overrides remain valid on
197
+ reload, even if their values never change.
198
+
199
+ ```ts
200
+ const secrets = await defineSecretSet(['DATABASE_URL'], {
201
+ organizationId: 'organization-id',
202
+ projectId: 'project-id',
203
+ writeToProcessEnv: false
204
+ })
205
+
206
+ await secrets.reload()
207
+ ```
208
+
209
+ `writeToProcessEnv: false` keeps fetched values within the set. Existing local
210
+ overrides still apply. Use this for multiple projects/environments in one
211
+ process and for build-time frontend configuration. With mirroring enabled,
212
+ `process.env` holds the most recently written values; each set retains its own
213
+ snapshot regardless of other sets' writes.
214
+
215
+ If another part of the app changes a mirrored environment value, it becomes a
216
+ local override. Reassigning exactly the same string is indistinguishable from
217
+ leaving the mirror untouched; use an explicit custom loader with isolated sets
218
+ when that distinction matters. Write tracking is shared across package copies
219
+ within the same JavaScript realm, not across processes or worker threads.
220
+
221
+ Reload refreshes keys without genuine local overrides. Concurrent reloads
222
+ share one operation, using the first caller's timeout and signal. Failed,
223
+ cancelled, or invalid refreshes preserve the previous cache. Validation and
224
+ cancellation checks happen before values are committed. `secrets()`, `sources()`,
225
+ and `parsed()` return copies.
226
+
227
+ ## Deadlines and cancellation
228
+
229
+ `timeoutMs` defaults to 30,000. The overall deadline covers authentication,
230
+ fetching, retries, response body reads, and schema validation. `signal` accepts
231
+ an `AbortSignal`; subprocesses and HTTP requests receive cancellation.
232
+
233
+ ```ts
234
+ const controller = new AbortController()
235
+ const secrets = await defineSecretSet(['APP_SECRET'], {
236
+ organizationId: 'organization-id',
237
+ projectId: 'project-id',
238
+ timeoutMs: 10_000,
239
+ signal: controller.signal
240
+ })
241
+
242
+ await secrets.reload({ timeoutMs: 5_000, signal: anotherController.signal })
243
+ ```
244
+
245
+ A reload can override the original timeout/signal. Custom loaders should honor
246
+ `context.signal`. A loader that ignores cancellation may continue its own work,
247
+ but its late result cannot update the secret set.
248
+
249
+ ## Schema validation and typed values
250
+
251
+ Use an optional Zod schema (or an object implementing `parseAsync`) to validate
252
+ the complete merged record. Raw secret methods and environment mirroring retain
253
+ strings. `parsed()` returns the inferred schema output, including transforms.
254
+
255
+ ```ts
256
+ import { z } from 'zod'
257
+ import { defineSecretSet } from 'hanzio/secrets'
258
+
259
+ const secrets = await defineSecretSet(['DATABASE_URL', 'PORT', 'FLAGS'], {
260
+ organizationId: 'organization-id',
261
+ projectId: 'project-id',
262
+ writeToProcessEnv: false,
263
+ schema: z.object({
264
+ DATABASE_URL: z.url(),
265
+ PORT: z.coerce.number().int().positive(),
266
+ FLAGS: z.string().transform(JSON.parse).pipe(z.object({ debug: z.boolean() }))
267
+ })
268
+ })
269
+
270
+ const port: number = secrets.parsed().PORT
271
+ const rawPort: string = secrets.secret('PORT')
272
+ ```
273
+
274
+ All configured keys must have nonempty strings before schema validation.
275
+ Schema output must be structured-cloneable. Validation errors omit input
276
+ values and schema error messages, which may themselves contain secrets.
277
+
278
+ ## Sources, logs, and errors
279
+
280
+ Successful loads emit `Loaded secrets` with environment, count, duration, and
281
+ counts for the sources that actually supplied the final values. Nothing is
282
+ reported as successfully loaded before fetching and validation finish.
283
+
284
+ ```text
285
+ Loaded secrets environment=dev count=3 source=process.env + Infisical HTTP durationMs=120
286
+ ```
287
+
288
+ Use `secrets.sources()` for per-key source labels. Built-in labels are
289
+ `process.env`, `Infisical HTTP`, and `Cloudflare Worker bindings`.
290
+ Organization-scoped user loads use
291
+ `Infisical CLI session (organization=<id> project=<id> environment=<slug>)`;
292
+ this indicates CLI authentication followed by HTTP fetching. The same label
293
+ appears in source counts and per-key metadata, without tokens or secret values.
294
+ A custom loader defaults to `custom loader`; set its `source` property or call
295
+ `context.reportSource('provider name')` for a dynamic source. Source labels are
296
+ caller-controlled metadata: never put secret values in them.
297
+
298
+ Set `logger: false` to disable logs, or pass an object with `info(message,
299
+ attributes)` and `error(message, attributes)` methods. The `sources` attribute
300
+ is a source-to-count object. Failures log only environment, count, duration,
301
+ and an error code. Logger exceptions do not change the load result.
302
+
303
+ `SecretLoadError` exposes a stable `code`, and optionally HTTP `operation` and
304
+ `status`. Codes include `CONFIGURATION`, `AUTHENTICATION`, `HTTP`, `CLI`,
305
+ `VALIDATION`, `MISSING_SECRETS`, `TIMEOUT`, `ABORTED`, and `LOADER`. HTTP and CLI
306
+ boundaries omit raw requests, responses, subprocess output, and underlying
307
+ causes. Custom-loader exceptions are replaced with a generic safe error.
308
+
309
+ ## Explicit loaders
310
+
311
+ The `loader` option replaces the default Infisical strategy:
312
+
313
+ - `infisicalLoader({ organizationId, projectId, clientId, clientSecret, siteUrl?, secretPath?, timeoutMs? })`
314
+ uses explicit credentials over HTTP. The enclosing set's deadline and signal
315
+ still apply. Read credentials from the environment or another secret store;
316
+ do not hard-code them.
317
+ - `processEnvLoader` uses only genuine process environment values. Reload
318
+ preserves unchanged overrides. It does not adopt another set's mirrored values.
319
+ - `cloudflareWorkerEnvLoader(bindings)` reads a bindings object. Strings are
320
+ retained; numbers and booleans become strings; JSON objects are serialized.
321
+ Schema validation can turn them into typed values afterward.
322
+
323
+ ## Cloudflare Workers
324
+
325
+ Use `hanzio/secrets/worker` for a runtime-neutral entry point with no CLI or
326
+ HTTP adapter dependency and no reliance on `process.env` or Node compatibility.
327
+ This entry point requires an explicit loader and does not read or write process
328
+ environment variables. Pass the environment option explicitly when needed.
329
+
330
+ ```ts
331
+ import { defineSecretSet, cloudflareWorkerEnvLoader } from 'hanzio/secrets/worker'
332
+
333
+ export default {
334
+ async fetch(request: Request, env: { APP_SECRET: string }) {
335
+ const secrets = await defineSecretSet(['APP_SECRET'], {
336
+ loader: cloudflareWorkerEnvLoader(env),
337
+ environment: 'prod',
338
+ logger: false
339
+ })
340
+ // Use secrets.secret('APP_SECRET') on the server.
341
+ return new Response('Ready')
342
+ }
343
+ }
344
+ ```
345
+
346
+ Keep tenant/request-specific bindings in separate sets rather than caching
347
+ them in a module-level singleton.
348
+
349
+ ## Vite: public values only
350
+
351
+ The default Vite helpers accept only sets consisting of `VITE_` keys. A set
352
+ containing any other key is rejected unless you explicitly supply `publicKeys`.
353
+ An explicit list exports only those keys; it may intentionally include a
354
+ non-prefixed public key. Vite built-in constants cannot be overridden.
355
+
356
+ ```ts
357
+ import { defineConfig } from 'vite'
358
+ import { defineSecretSet } from 'hanzio/secrets'
359
+ import { viteSecretSetPlugin } from 'hanzio/secrets/vite'
360
+
361
+ const frontend = await defineSecretSet(['VITE_POSTHOG_API_KEY', 'VITE_POSTHOG_HOST'], {
362
+ organizationId: 'organization-id',
363
+ projectId: 'project-id',
364
+ writeToProcessEnv: false
365
+ })
366
+
367
+ export default defineConfig({
368
+ plugins: [viteSecretSetPlugin(frontend)]
369
+ })
370
+ ```
371
+
372
+ For a mixed set, use `viteSecretSetPlugin(set, { publicKeys: ['VITE_PUBLIC_KEY'] })`.
373
+ `getViteDefine(set, options)` accepts the same options for manual integration.
374
+ Every selected value is public and may be included in browser code; `VITE_`
375
+ is a naming convention, not encryption. Keep private credentials on the server.
376
+ Vite mode and `NODE_ENV` are separate; pass `environment: 'staging'` explicitly
377
+ when building a staging configuration.
378
+
379
+ ## Migration notes
380
+
381
+ - Add `organizationId` to every Infisical configuration, including explicit HTTP
382
+ loaders. CLI authentication now always uses the organization-scoped session
383
+ flow; the unscoped CLI export fallback has been removed.
384
+
385
+ - Vite calls that exposed non-`VITE_` keys now fail. Rename public keys or pass
386
+ an explicit `publicKeys` list. Never allowlist server credentials.
387
+ - Unknown/empty explicit environments now fail. Use `{ slug: 'custom-name' }`
388
+ for custom environments, or unset `SECRETS_ENV` to use defaults.
389
+ - A second set no longer silently inherits remote values mirrored by a first
390
+ set. Give each set its loader configuration; use `writeToProcessEnv: false`
391
+ to keep remote values isolated.
392
+ - `reload()` now preserves genuine environment overrides and shares concurrent
393
+ calls. Loaders receive only keys requiring remote loading, including on reload.
394
+ - Success logging changes from `Loading secrets` to `Loaded secrets`, with
395
+ actual source counts and duration. Failures emit a separate sanitized event.
396
+ - Infisical failures now use `SecretLoadError`; code inspecting raw API-wrapper
397
+ errors must switch to `code`, `operation`, and `status`.
398
+ - Custom loader context `environment` is now a string to accommodate custom slugs.
399
+
400
+ ## Validation
401
+
402
+ `bun test` covers ownership, reload races, validation, source reports, safe
403
+ errors, authentication selection, paths, deadlines, and cancellation.
404
+ Organization tests cover concurrent sets, rereading the session on reload, MFA,
405
+ expired tokens, denied access, and sanitized errors.
406
+ `bun run test:secrets:runtime` builds and packs the package, exercises a fake
407
+ CLI under Node and Bun, tests concurrent organization scopes against a local
408
+ HTTP server, checks a Vite browser bundle for a private fixture,
409
+ and executes the Worker entry point in local workerd through Miniflare without
410
+ Node compatibility. Runtime verification requires macOS/Linux, Node 22.12+, and Bun on PATH;
411
+ it never uses a real Infisical session or deploys a Worker.
412
+ The CLI, runtime, and Worker fixtures live in `scripts/fixtures` so their code
413
+ can be read and edited directly.