hanzio 1.0.10 → 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 (71) hide show
  1. package/README.md +70 -4
  2. package/dist/api-wrapper/actions-impl.d.ts +4 -0
  3. package/dist/api-wrapper/builders.d.ts +23 -0
  4. package/dist/api-wrapper/create-client.d.ts +6 -0
  5. package/dist/api-wrapper/error-reporting.d.ts +7 -0
  6. package/dist/api-wrapper/errors.d.ts +64 -0
  7. package/dist/api-wrapper/headers.d.ts +10 -0
  8. package/dist/api-wrapper/index.d.ts +5 -84
  9. package/dist/api-wrapper/redaction.d.ts +2 -0
  10. package/dist/api-wrapper/shared.d.ts +16 -0
  11. package/dist/api-wrapper/size.d.ts +1 -0
  12. package/dist/api-wrapper/test-helpers.d.ts +6 -0
  13. package/dist/api-wrapper/transport.d.ts +55 -0
  14. package/dist/api-wrapper/types.d.ts +154 -0
  15. package/dist/api-wrapper/zod-issues.d.ts +17 -0
  16. package/dist/cache/index.d.ts +26 -0
  17. package/dist/secrets/core.d.ts +47 -0
  18. package/dist/secrets/deadline.d.ts +6 -0
  19. package/dist/secrets/environment.d.ts +9 -0
  20. package/dist/secrets/errors.d.ts +11 -0
  21. package/dist/secrets/index.d.ts +14 -23
  22. package/dist/secrets/infisical-cli.d.ts +6 -0
  23. package/dist/secrets/infisical-config.d.ts +17 -0
  24. package/dist/secrets/infisical-session.d.ts +4 -0
  25. package/dist/secrets/infisical.d.ts +14 -2
  26. package/dist/secrets/loaders.d.ts +9 -0
  27. package/dist/secrets/vite.d.ts +7 -3
  28. package/dist/secrets/worker-loader.d.ts +9 -0
  29. package/dist/secrets/worker.d.ts +3 -0
  30. package/dist/src/api-wrapper/index.js +1059 -177
  31. package/dist/src/index.js +2807 -1397
  32. package/dist/src/jwt/index.js +2 -2
  33. package/dist/src/p-queue/index.js +3 -3
  34. package/dist/src/secrets/index.js +1610 -407
  35. package/dist/src/secrets/infisical.js +1352 -214
  36. package/dist/src/secrets/loaders.js +1529 -0
  37. package/dist/src/secrets/vite.js +33 -11
  38. package/dist/src/secrets/worker.js +275 -0
  39. package/dist/src/sitemap/index.js +1 -1
  40. package/dist/src/zod/index.js +7 -7
  41. package/dist/typedswitch/index.d.ts +9 -5
  42. package/package.json +12 -3
  43. package/src/api-wrapper/actions-impl.ts +100 -0
  44. package/src/api-wrapper/builders.ts +200 -0
  45. package/src/api-wrapper/create-client.ts +393 -0
  46. package/src/api-wrapper/error-reporting.ts +31 -0
  47. package/src/api-wrapper/errors.ts +231 -0
  48. package/src/api-wrapper/headers.ts +79 -0
  49. package/src/api-wrapper/index.ts +5 -492
  50. package/src/api-wrapper/redaction.ts +62 -0
  51. package/src/api-wrapper/shared.ts +25 -0
  52. package/src/api-wrapper/size.ts +19 -0
  53. package/src/api-wrapper/test-helpers.ts +25 -0
  54. package/src/api-wrapper/transport.ts +350 -0
  55. package/src/api-wrapper/types.ts +259 -0
  56. package/src/api-wrapper/zod-issues.ts +157 -0
  57. package/src/cache/index.ts +228 -61
  58. package/src/secrets/core.ts +348 -0
  59. package/src/secrets/deadline.ts +69 -0
  60. package/src/secrets/environment.ts +69 -0
  61. package/src/secrets/errors.ts +29 -0
  62. package/src/secrets/index.ts +81 -140
  63. package/src/secrets/infisical-cli.ts +99 -0
  64. package/src/secrets/infisical-config.ts +70 -0
  65. package/src/secrets/infisical-session.ts +42 -0
  66. package/src/secrets/infisical.ts +219 -35
  67. package/src/secrets/loaders.ts +43 -0
  68. package/src/secrets/vite.ts +44 -15
  69. package/src/secrets/worker-loader.ts +56 -0
  70. package/src/secrets/worker.ts +4 -0
  71. package/src/typedswitch/index.ts +80 -69
@@ -1,153 +1,94 @@
1
- import { colorize, createCoolLogger } from '../cool-console-log'
2
- import { loadInfisicalSecrets } from './infisical'
3
-
4
- const DEFAULT_INFISICAL_SITE_URL = 'https://eu.infisical.com'
5
- const logger = createCoolLogger()
6
-
7
- export type SecretEnvironment = 'dev' | 'staging' | 'prod'
8
-
9
- export type DefineSecretSetOptions<SecretKey extends string> = {
10
- readonly projectId: string
11
- readonly siteUrl?: string
12
- readonly environment?: SecretEnvironment | (() => SecretEnvironment)
13
- readonly clientIdEnvKey?: string
14
- readonly clientSecretEnvKey?: string
15
- readonly loader?: SecretSetLoader<SecretKey>
16
- }
17
-
18
- export type SecretSetLoaderContext<SecretKey extends string> = {
19
- readonly keys: readonly SecretKey[]
20
- readonly projectId: string
21
- readonly environment: SecretEnvironment
22
- readonly siteUrl: string
23
- readonly clientIdEnvKey: string
24
- readonly clientSecretEnvKey: string
25
- }
26
-
27
- export type SecretSetLoader<SecretKey extends string> = (
28
- context: SecretSetLoaderContext<SecretKey>
29
- ) => Promise<Partial<Record<SecretKey, string>>>
30
-
31
- export type SecretSet<SecretKey extends string> = {
32
- secret: (key: SecretKey) => string
33
- secrets: () => Record<SecretKey, string>
34
- reload: () => Promise<SecretSet<SecretKey>>
35
- }
36
-
1
+ import {
2
+ defineSecretSet as defineCoreSecretSet,
3
+ type CommonSecretSetOptions,
4
+ type CustomLoaderDefineSecretSetOptions,
5
+ type SecretSet,
6
+ type SecretSetLoader
7
+ } from './core'
8
+ import {
9
+ getProcessEnvironment,
10
+ resolveSecretEnvironment,
11
+ type SecretEnvironment
12
+ } from './environment'
13
+ import {
14
+ DEFAULT_INFISICAL_SITE_URL,
15
+ loadInfisicalSecrets,
16
+ type InfisicalAuthMode
17
+ } from './infisical'
18
+ import {
19
+ validateInfisicalOrganizationId,
20
+ type InfisicalProjectOptions
21
+ } from './infisical-config'
22
+
23
+ export * from './core'
37
24
  export * from './infisical'
25
+ export * from './loaders'
38
26
  export * from './vite'
27
+ export type { SecretRequestOptions } from './deadline'
28
+
29
+ export type InfisicalDefineSecretSetOptions<
30
+ K extends string = string,
31
+ Parsed = Record<K, string>
32
+ > = CommonSecretSetOptions<K, Parsed> &
33
+ InfisicalProjectOptions & {
34
+ readonly auth?: InfisicalAuthMode
35
+ readonly clientIdEnvKey?: string
36
+ readonly clientSecretEnvKey?: string
37
+ readonly loader?: undefined
38
+ }
39
+
40
+ export type DefineSecretSetOptions<
41
+ K extends string,
42
+ Parsed = Record<K, string>
43
+ > =
44
+ | InfisicalDefineSecretSetOptions<K, Parsed>
45
+ | CustomLoaderDefineSecretSetOptions<K, Parsed>
39
46
 
40
47
  export function getSecretEnvironment(): SecretEnvironment {
41
- const env = process.env.SECRETS_ENV || process.env.NODE_ENV
42
- if (env === 'production' || env === 'prod') return 'prod'
43
- if (env === 'staging') return 'staging'
44
- return 'dev'
48
+ return resolveSecretEnvironment(
49
+ undefined,
50
+ getProcessEnvironment()
51
+ ) as SecretEnvironment
45
52
  }
46
53
 
47
54
  export async function defineSecretSet<
48
- const SecretKeys extends readonly string[]
55
+ const Keys extends readonly string[],
56
+ Parsed = Record<Keys[number], string>
49
57
  >(
50
- keys: SecretKeys,
51
- options: DefineSecretSetOptions<SecretKeys[number]>
52
- ): Promise<SecretSet<SecretKeys[number]>> {
53
- type SecretKey = SecretKeys[number]
54
-
55
- const keySet = [...new Set(keys)] as SecretKey[]
56
- const loader = options.loader ?? loadInfisicalSecrets
57
- let cachedSecrets: Record<SecretKey, string> | null = null
58
-
59
- const loadSecrets = async (force = false) => {
60
- const envSecrets = readEnvSecrets(keySet, force ? cachedSecrets : null)
61
- const keysMissingFromEnv = keySet.filter((key) => !envSecrets[key])
62
- const environment = resolveEnvironment(options.environment)
63
-
64
- logSecretEnvironment(environment, keySet.length)
65
-
66
- const loadedSecrets: Partial<Record<SecretKey, string>> =
67
- keysMissingFromEnv.length === 0 && !force
68
- ? {}
69
- : await loader({
70
- keys: keySet,
71
- projectId: options.projectId,
72
- environment,
73
- siteUrl: options.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
74
- clientIdEnvKey: options.clientIdEnvKey ?? 'INFISICAL_CLIENT_ID',
75
- clientSecretEnvKey:
76
- options.clientSecretEnvKey ?? 'INFISICAL_CLIENT_SECRET'
77
- })
78
-
79
- const missingKeys = keySet.filter(
80
- (key) => !(envSecrets[key] ?? loadedSecrets[key])
81
- )
82
- if (missingKeys.length > 0) {
83
- throw new Error(`Missing secrets: ${missingKeys.join(', ')}`)
84
- }
85
-
86
- const nextSecrets = Object.fromEntries(
87
- keySet.map((key) => [
88
- key,
89
- (envSecrets[key] ?? loadedSecrets[key]) as string
90
- ])
91
- ) as Record<SecretKey, string>
92
-
93
- for (const [key, value] of Object.entries(nextSecrets)) {
94
- process.env[key] = value as string
95
- }
96
- cachedSecrets = nextSecrets
97
- return nextSecrets
58
+ keys: Keys,
59
+ options: DefineSecretSetOptions<Keys[number], Parsed>
60
+ ): Promise<SecretSet<Keys[number], Parsed>> {
61
+ let loader: SecretSetLoader<Keys[number]>
62
+
63
+ if (!options.loader) {
64
+ // Validate even when environment overrides satisfy every key.
65
+ validateInfisicalOrganizationId(options.organizationId)
66
+ loader = createDefaultInfisicalLoader(options)
67
+ } else {
68
+ loader = options.loader
98
69
  }
99
70
 
100
- const secretSet: SecretSet<SecretKey> = {
101
- secret(key) {
102
- const value = cachedSecrets?.[key] ?? process.env[key]
103
- if (!value) {
104
- throw new Error(`Secret ${key} not found`)
105
- }
106
- return value
107
- },
108
- secrets() {
109
- if (!cachedSecrets) {
110
- throw new Error('Secrets not initialized.')
111
- }
112
- return cachedSecrets
113
- },
114
- async reload() {
115
- await loadSecrets(true)
116
- return secretSet
117
- }
118
- }
119
-
120
- await loadSecrets()
121
- return secretSet
122
- }
123
-
124
- function resolveEnvironment(
125
- environment: DefineSecretSetOptions<string>['environment']
126
- ): SecretEnvironment {
127
- if (typeof environment === 'function') return environment()
128
- return environment ?? getSecretEnvironment()
129
- }
130
-
131
- function logSecretEnvironment(
132
- environment: SecretEnvironment,
133
- secretCount: number
134
- ): void {
135
- logger.info(colorize('Loading secrets', 'cyan', 'bold'), {
136
- environment: colorize(environment, 'brightCyan', 'bold'),
137
- count: secretCount
138
- })
71
+ // Supply process.env only from this Node/Bun entry point, keeping the core portable.
72
+ return defineCoreSecretSet(
73
+ keys,
74
+ { ...options, loader },
75
+ getProcessEnvironment()
76
+ )
139
77
  }
140
78
 
141
- function readEnvSecrets<SecretKey extends string>(
142
- keys: readonly SecretKey[],
143
- previousSecrets: Record<SecretKey, string> | null = null
144
- ): Partial<Record<SecretKey, string>> {
145
- return Object.fromEntries(
146
- keys
147
- .filter((key) => {
148
- const value = process.env[key]
149
- return value && value !== previousSecrets?.[key]
150
- })
151
- .map((key) => [key, process.env[key] as string])
152
- ) as Partial<Record<SecretKey, string>>
79
+ function createDefaultInfisicalLoader<K extends string, Parsed>(
80
+ options: InfisicalDefineSecretSetOptions<K, Parsed>
81
+ ): SecretSetLoader<K> {
82
+ return (context) =>
83
+ loadInfisicalSecrets({
84
+ ...context,
85
+ projectId: options.projectId,
86
+ organizationId: options.organizationId,
87
+ siteUrl: options.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
88
+ secretPath: options.secretPath,
89
+ auth: options.auth,
90
+ clientIdEnvKey: options.clientIdEnvKey ?? 'INFISICAL_CLIENT_ID',
91
+ clientSecretEnvKey:
92
+ options.clientSecretEnvKey ?? 'INFISICAL_CLIENT_SECRET'
93
+ })
153
94
  }
@@ -0,0 +1,99 @@
1
+ import type { SecretRequestOptions } from './deadline'
2
+ import { getProcessEnvironment } from './environment'
3
+ import { SecretLoadError } from './errors'
4
+
5
+ /** Read the active CLI session without a shell or credential-bearing errors. */
6
+ export async function readInfisicalCliToken(
7
+ options: SecretRequestOptions & { siteUrl: string; signal: AbortSignal }
8
+ ): Promise<string> {
9
+ // Import subprocess support only when the CLI is actually selected.
10
+ const { execFile } = await import('node:child_process').catch(() => {
11
+ throw new SecretLoadError(
12
+ 'CLI',
13
+ 'Infisical CLI fallback requires a runtime with subprocess support. Provide Infisical client credentials or a custom secret loader.'
14
+ )
15
+ })
16
+
17
+ options.signal.throwIfAborted()
18
+
19
+ const env: Record<string, string | undefined> = {
20
+ ...getProcessEnvironment(),
21
+ INFISICAL_DISABLE_UPDATE_CHECK: 'true'
22
+ }
23
+
24
+ // Inherited machine tokens would override the CLI's signed-in user session.
25
+ // Remove them from the child copy without changing the parent's environment.
26
+ delete env.INFISICAL_TOKEN
27
+ delete env.INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN
28
+ delete env.TOKEN
29
+
30
+ try {
31
+ const stdout = await new Promise<string>((resolve, reject) => {
32
+ const child = execFile(
33
+ 'infisical',
34
+ [
35
+ 'user',
36
+ 'get',
37
+ 'token',
38
+ '--plain',
39
+ `--domain=${options.siteUrl}`,
40
+ '--silent'
41
+ ],
42
+ {
43
+ env,
44
+ encoding: 'utf8',
45
+ shell: false,
46
+ timeout: options.timeoutMs ?? 30_000,
47
+ signal: options.signal,
48
+ killSignal: 'SIGKILL',
49
+ maxBuffer: 10 * 1024 * 1024,
50
+ windowsHide: true
51
+ },
52
+ (error, output) => {
53
+ if (error) {
54
+ reject(error)
55
+ } else {
56
+ resolve(output)
57
+ }
58
+ }
59
+ )
60
+
61
+ // Startup cannot wait for interactive input.
62
+ child.stdin?.end()
63
+ })
64
+ return stdout.trim()
65
+ } catch (error) {
66
+ if (options.signal.aborted) {
67
+ throw options.signal.reason
68
+ }
69
+
70
+ // Subprocess errors can contain secret stdout/stderr, so rebuild the error.
71
+ const failure = error as { code?: string | number; killed?: boolean } | null
72
+
73
+ if (failure?.code === 'ENOENT') {
74
+ throw new SecretLoadError(
75
+ 'CLI',
76
+ 'Infisical CLI not found. Install infisical and ensure it is on PATH, or provide Infisical client credentials.'
77
+ )
78
+ }
79
+
80
+ if (failure?.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
81
+ throw new SecretLoadError(
82
+ 'CLI',
83
+ 'Infisical CLI output exceeded the 10 MiB limit.'
84
+ )
85
+ }
86
+
87
+ if (failure?.killed) {
88
+ throw new SecretLoadError(
89
+ 'TIMEOUT',
90
+ `Infisical CLI timed out after ${(options.timeoutMs ?? 30_000) / 1000} seconds or was terminated.`
91
+ )
92
+ }
93
+
94
+ throw new SecretLoadError(
95
+ 'CLI',
96
+ `Infisical CLI session token failed. Run infisical login --domain=${options.siteUrl} and verify access to the configured organization, project, and environment.`
97
+ )
98
+ }
99
+ }
@@ -0,0 +1,70 @@
1
+ import type { SecretRequestOptions } from './deadline'
2
+ import { SecretLoadError } from './errors'
3
+
4
+ /** Project scope shared by the public Infisical loaders. */
5
+ export type InfisicalProjectOptions = {
6
+ readonly projectId: string
7
+
8
+ /** Scope CLI user authentication; machine identities already have an organization. */
9
+ readonly organizationId: string
10
+ readonly siteUrl?: string
11
+ readonly secretPath?: string
12
+ }
13
+
14
+ export type InfisicalScope<K extends string> = InfisicalProjectOptions &
15
+ SecretRequestOptions & {
16
+ readonly keys: readonly K[]
17
+ readonly environment: string
18
+ readonly siteUrl: string
19
+ }
20
+
21
+ export function validateInfisicalScope(options: InfisicalScope<string>): void {
22
+ validateInfisicalOrganizationId(options.organizationId)
23
+
24
+ let url: URL
25
+
26
+ try {
27
+ url = new URL(options.siteUrl)
28
+ } catch {
29
+ throw new SecretLoadError('CONFIGURATION', 'Invalid Infisical siteUrl.')
30
+ }
31
+
32
+ // Keep credentials and other sensitive URL components out of diagnostics.
33
+ if (
34
+ !['http:', 'https:'].includes(url.protocol) ||
35
+ url.username ||
36
+ url.password ||
37
+ url.search ||
38
+ url.hash
39
+ ) {
40
+ throw new SecretLoadError(
41
+ 'CONFIGURATION',
42
+ 'Infisical siteUrl must be an HTTP(S) URL without credentials, query, or fragment.'
43
+ )
44
+ }
45
+
46
+ if (
47
+ !options.projectId.trim() ||
48
+ !options.environment.trim() ||
49
+ !(options.secretPath ?? '/').startsWith('/')
50
+ ) {
51
+ throw new SecretLoadError(
52
+ 'CONFIGURATION',
53
+ 'Infisical requires a projectId, environment, and absolute secretPath.'
54
+ )
55
+ }
56
+ }
57
+
58
+ /** Validate at configuration time, including for JavaScript callers and local-only loads. */
59
+ export function validateInfisicalOrganizationId(organizationId: unknown): void {
60
+ if (
61
+ typeof organizationId !== 'string' ||
62
+ !organizationId.trim() ||
63
+ /\s/.test(organizationId)
64
+ ) {
65
+ throw new SecretLoadError(
66
+ 'CONFIGURATION',
67
+ 'Infisical organizationId is required and must be a nonempty ID without whitespace.'
68
+ )
69
+ }
70
+ }
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod'
2
+ import { SecretLoadError } from './errors'
3
+
4
+ const sessionClaimsSchema = z.object({
5
+ organizationId: z.string().optional(),
6
+ subOrganizationId: z.string().optional(),
7
+ exp: z.number().finite().optional()
8
+ })
9
+
10
+ /** Read routing metadata only. JWT signature and permissions are verified by Infisical. */
11
+ export function parseInfisicalSessionToken(token: string): {
12
+ organizationId?: string
13
+ } {
14
+ let claims: z.infer<typeof sessionClaimsSchema>
15
+
16
+ try {
17
+ // Reject empty, formatted, or multiline CLI output before making an HTTP request.
18
+ if (!/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token)) {
19
+ throw new Error('Invalid token format')
20
+ }
21
+
22
+ const payload = token.split('.')[1]!
23
+ const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
24
+
25
+ claims = sessionClaimsSchema.parse(JSON.parse(json))
26
+ } catch {
27
+ throw new SecretLoadError(
28
+ 'AUTHENTICATION',
29
+ 'Infisical returned an invalid user session token. Run infisical login for the configured domain.'
30
+ )
31
+ }
32
+
33
+ if (claims.exp !== undefined && claims.exp <= Date.now() / 1000) {
34
+ throw new SecretLoadError(
35
+ 'AUTHENTICATION',
36
+ 'Infisical user session has expired. Run infisical login for the configured domain.'
37
+ )
38
+ }
39
+
40
+ // For sub-organizations, the root organization claim does not identify the scope.
41
+ return { organizationId: claims.subOrganizationId ?? claims.organizationId }
42
+ }