hanzio 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +24 -10
- package/dist/api-wrapper/endpoint.d.ts +8 -5
- package/dist/api-wrapper/transport.d.ts +1 -0
- package/dist/api-wrapper/types.d.ts +7 -0
- package/dist/{index-j1ybktbr.js → index-zbj9s3s3.js} +6 -4
- package/dist/src/api-wrapper/index.js +1 -1
- package/dist/src/index.js +8 -2
- package/dist/src/secrets/index.js +1 -1
- package/dist/typedswitch/index.d.ts +55 -34
- package/package.json +11 -3
- package/src/api-wrapper/README.md +741 -0
- package/src/api-wrapper/SKILL.md +256 -0
- package/src/api-wrapper/create-client.ts +9 -3
- package/src/api-wrapper/endpoint.ts +19 -5
- package/src/api-wrapper/transport.ts +6 -2
- package/src/api-wrapper/types.ts +13 -0
- package/src/crypto/README.md +144 -0
- package/src/format/README.md +120 -0
- package/src/i18n/README.md +65 -0
- package/src/jwt/README.md +70 -0
- package/src/p-queue/README.md +237 -0
- package/src/secrets/README.md +413 -0
- package/src/sitemap/README.md +61 -0
- package/src/storage/README.md +71 -0
- package/src/typedswitch/README.md +335 -0
- package/src/typedswitch/index.ts +97 -83
|
@@ -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.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Discover and crawl XML sitemaps for URL discovery; uses the local p-queue utility for concurrency.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# hanzio/sitemap
|
|
6
|
+
|
|
7
|
+
`getDomainSitemap(domain, options)` finds a domain's sitemaps (the `Sitemap:` lines in robots.txt first, then common locations like `/sitemap.xml`) and crawls them with bounded parallelism using the local `hanzio/p-queue` utility.
|
|
8
|
+
|
|
9
|
+
- A document whose root element is `<sitemapindex>` is treated as a list of child sitemaps. A document whose root element is `<urlset>` is treated as a list of pages.
|
|
10
|
+
- `<loc>` values are XML-entity decoded, and CDATA is supported.
|
|
11
|
+
- Gzipped sitemaps (`.xml.gz`) are decompressed with `DecompressionStream`.
|
|
12
|
+
- Every request has its own timeout (`AbortSignal.timeout`).
|
|
13
|
+
|
|
14
|
+
Requires runtime `fetch`, `AbortSignal.timeout` and `DecompressionStream` (Node 18+, Bun, modern browsers), or pass your own `fetch`.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { getDomainSitemap, getDomainSitemapEntries } from 'hanzio/sitemap'
|
|
18
|
+
|
|
19
|
+
const urls = await getDomainSitemap('example.com', {
|
|
20
|
+
filterIndexes: 'posts', // substring, RegExp or (url) => boolean
|
|
21
|
+
filterUrls: /\/blog\//,
|
|
22
|
+
maxUrls: 1000
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// With lastmod
|
|
26
|
+
const entries = await getDomainSitemapEntries('example.com')
|
|
27
|
+
// [{ loc: 'https://example.com/a', lastmod: '2024-01-05' }, ...]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Options (`SitemapOptions`)
|
|
31
|
+
|
|
32
|
+
| Option | Default | Description |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `filterIndexes` | none | Only follow child sitemaps that match (substring, RegExp or predicate). |
|
|
35
|
+
| `filterUrls` | none | Only keep page URLs that match. |
|
|
36
|
+
| `concurrency` | `10` | The maximum number of sitemap fetches that run at the same time. |
|
|
37
|
+
| `maxDepth` | `5` | The maximum sitemap-index nesting depth. |
|
|
38
|
+
| `maxUrls` | none | Stop crawling after this many URLs have been collected. |
|
|
39
|
+
| `timeout` | `10000` | The timeout for each request, in ms. |
|
|
40
|
+
| `userAgent` | `'SitemapCrawler/1.0'` | The `User-Agent` header sent with each request. |
|
|
41
|
+
| `fetch` | `globalThis.fetch` | A custom fetch implementation. |
|
|
42
|
+
| `onError` | `console.warn` | `(url, error) => void`, called when a robots.txt or sitemap request fails. |
|
|
43
|
+
| `respectRobots` | `false` | Drop page URLs that robots.txt disallows for `userAgent` (reuses the robots.txt fetched for discovery). A missing or failed robots.txt allows everything. |
|
|
44
|
+
|
|
45
|
+
Both functions throw `No sitemaps found for the domain` when discovery finds nothing.
|
|
46
|
+
|
|
47
|
+
## robots.txt
|
|
48
|
+
|
|
49
|
+
`parseRobotsTxt(body, userAgent = '*')` returns the rules for one crawler, following RFC 9309: the groups that name its product token (case-insensitive, so `MyBot/2.0` matches `User-agent: mybot`), merged, or else the `User-agent: *` groups. Directive names are case-insensitive and `#` comments are ignored. `sitemaps` lists every `Sitemap:` line.
|
|
50
|
+
|
|
51
|
+
`isAllowedByRobots(rules, url)` checks an absolute URL or a path (plus query string). The longest matching pattern wins, `Allow` wins a tie, `*` matches any characters and a trailing `$` anchors the end. An empty `Disallow:` allows everything, and `/robots.txt` is always allowed.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { isAllowedByRobots, parseRobotsTxt } from 'hanzio/sitemap'
|
|
55
|
+
|
|
56
|
+
const rules = parseRobotsTxt(body, 'MyBot/1.0')
|
|
57
|
+
// { allow: ['/cart/help'], disallow: ['/cart/'], sitemaps: [...], crawlDelaySec?: 5 }
|
|
58
|
+
|
|
59
|
+
isAllowedByRobots(rules, 'https://example.com/cart/add?id=1') // false
|
|
60
|
+
isAllowedByRobots(rules, '/cart/help') // true
|
|
61
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Typed persisted values over localStorage/sessionStorage or async stores like React Native AsyncStorage, with fallbacks that never throw.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# hanzio/storage
|
|
6
|
+
|
|
7
|
+
`createStorageItem` wraps one key in `localStorage` (or any `SyncStorageLike`) as a typed value. It never throws:
|
|
8
|
+
|
|
9
|
+
- A missing, corrupt (bad JSON) or invalid (`parse` throws) value reads as `fallback`.
|
|
10
|
+
- When the storage throws (quota exceeded, Safari private mode, sandboxed iframes), the latest `set`/`remove` is kept in memory for that item, so reads stay consistent for the session.
|
|
11
|
+
- Without an accessible `globalThis.localStorage` (SSR, Node, Bun, blocked access) it uses a shared in-memory store.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { createStorageItem } from 'hanzio/storage'
|
|
16
|
+
|
|
17
|
+
const PanelPrefs = z.object({ open: z.boolean(), width: z.number() })
|
|
18
|
+
|
|
19
|
+
const panelPrefs = createStorageItem({
|
|
20
|
+
key: 'panel-prefs',
|
|
21
|
+
fallback: { open: false, width: 320 },
|
|
22
|
+
parse: PanelPrefs.parse
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
panelPrefs.get() // { open: false, width: 320 }
|
|
26
|
+
panelPrefs.set({ open: true, width: 400 })
|
|
27
|
+
panelPrefs.remove()
|
|
28
|
+
|
|
29
|
+
// Another tab changed it (the `storage` event). Returns an unsubscribe function.
|
|
30
|
+
const stop = panelPrefs.subscribe((prefs) => console.log(prefs))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use `sessionStorage` or plain strings:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const sendOnEnter = createStorageItem({
|
|
37
|
+
key: 'send-on-enter',
|
|
38
|
+
fallback: false,
|
|
39
|
+
storage: sessionStorage,
|
|
40
|
+
serialize: String,
|
|
41
|
+
deserialize: (raw) => raw === 'true'
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Async stores
|
|
46
|
+
|
|
47
|
+
`createAsyncStorageItem` takes the same options, but `storage` is required and the methods return promises. It never rejects.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
51
|
+
import { createAsyncStorageItem } from 'hanzio/storage'
|
|
52
|
+
|
|
53
|
+
const readIds = createAsyncStorageItem({
|
|
54
|
+
key: '@notification_read_session_ids',
|
|
55
|
+
fallback: [] as string[],
|
|
56
|
+
storage: AsyncStorage,
|
|
57
|
+
parse: z.array(z.string()).parse
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
await readIds.set([...(await readIds.get()), 'abc'])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
| Export | Description |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `createStorageItem(options)` | `{ key, get, set, remove, subscribe }` over a `SyncStorageLike` (default `localStorage`). |
|
|
68
|
+
| `createAsyncStorageItem(options)` | `{ key, get, set, remove }` over an `AsyncStorageLike`. |
|
|
69
|
+
| `createMemoryStorage()` | A `Map`-backed `SyncStorageLike`, for tests and SSR. |
|
|
70
|
+
|
|
71
|
+
Options: `key`, `fallback`, `storage`, `parse` (validate the deserialized value; throwing means `fallback`), `serialize` (default `JSON.stringify`), `deserialize` (default `JSON.parse`).
|