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
@@ -0,0 +1,69 @@
1
+ import { SecretLoadError } from './errors'
2
+
3
+ export type SecretRequestOptions = {
4
+ readonly timeoutMs?: number
5
+ readonly signal?: AbortSignal
6
+ }
7
+
8
+ /** One deadline includes authentication, retries, body reads, and validation. */
9
+ export async function withSecretDeadline<T>(
10
+ options: SecretRequestOptions,
11
+ operation: (signal: AbortSignal) => Promise<T>
12
+ ): Promise<T> {
13
+ const timeoutMs = options.timeoutMs ?? 30_000
14
+
15
+ if (
16
+ !Number.isFinite(timeoutMs) ||
17
+ timeoutMs <= 0 ||
18
+ timeoutMs > 2_147_483_647
19
+ ) {
20
+ throw new SecretLoadError(
21
+ 'CONFIGURATION',
22
+ 'Secret timeoutMs must be a positive finite timer duration.'
23
+ )
24
+ }
25
+
26
+ // Normalize caller cancellation so arbitrary abort reasons cannot leak secrets.
27
+ const controller = new AbortController()
28
+ const cancel = () =>
29
+ controller.abort(
30
+ new SecretLoadError('ABORTED', 'Secret loading was cancelled.')
31
+ )
32
+
33
+ if (options.signal?.aborted) {
34
+ cancel()
35
+ } else {
36
+ options.signal?.addEventListener('abort', cancel, { once: true })
37
+ }
38
+
39
+ const timer = setTimeout(
40
+ () =>
41
+ controller.abort(
42
+ new SecretLoadError(
43
+ 'TIMEOUT',
44
+ `Secret loading timed out after ${timeoutMs}ms.`
45
+ )
46
+ ),
47
+ timeoutMs
48
+ )
49
+ let rejectOnAbort: () => void = () => {}
50
+
51
+ try {
52
+ const aborted = new Promise<never>((_, reject) => {
53
+ rejectOnAbort = () => reject(controller.signal.reason)
54
+ controller.signal.addEventListener('abort', rejectOnAbort, { once: true })
55
+ })
56
+
57
+ if (controller.signal.aborted) {
58
+ throw controller.signal.reason
59
+ }
60
+
61
+ // Reject promptly even if a custom loader ignores the abort signal.
62
+ return await Promise.race([operation(controller.signal), aborted])
63
+ } finally {
64
+ // Neither completed loads nor failures should retain listeners or timers.
65
+ clearTimeout(timer)
66
+ options.signal?.removeEventListener('abort', cancel)
67
+ controller.signal.removeEventListener('abort', rejectOnAbort)
68
+ }
69
+ }
@@ -0,0 +1,69 @@
1
+ import { SecretLoadError } from './errors'
2
+
3
+ export type SecretEnvironment = 'dev' | 'staging' | 'prod'
4
+
5
+ export type SecretEnvironmentOption =
6
+ | SecretEnvironment
7
+ | { readonly slug: string }
8
+ | (() => SecretEnvironment | { readonly slug: string })
9
+
10
+ export type EnvironmentVariables = Record<string, string | undefined>
11
+
12
+ export function getProcessEnvironment(): EnvironmentVariables | undefined {
13
+ return typeof process === 'undefined' ? undefined : process.env
14
+ }
15
+
16
+ export function resolveSecretEnvironment(
17
+ option?: SecretEnvironmentOption,
18
+ env?: EnvironmentVariables
19
+ ): string {
20
+ // Resolve callbacks once; every reload stays in the same environment.
21
+ const explicit = typeof option === 'function' ? option() : option
22
+
23
+ if (typeof explicit === 'object' && explicit !== null) {
24
+ if (
25
+ typeof explicit.slug !== 'string' ||
26
+ !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(explicit.slug)
27
+ ) {
28
+ throw new SecretLoadError(
29
+ 'CONFIGURATION',
30
+ 'Custom secret environment requires a valid slug.'
31
+ )
32
+ }
33
+
34
+ return explicit.slug
35
+ }
36
+
37
+ // Explicit configuration is strict. Typos must not silently select dev.
38
+ const selected = explicit ?? env?.SECRETS_ENV
39
+
40
+ if (selected !== undefined) {
41
+ if (selected === 'production' || selected === 'prod') {
42
+ return 'prod'
43
+ }
44
+
45
+ if (selected === 'development' || selected === 'dev') {
46
+ return 'dev'
47
+ }
48
+
49
+ if (selected === 'staging') {
50
+ return 'staging'
51
+ }
52
+
53
+ throw new SecretLoadError(
54
+ 'CONFIGURATION',
55
+ 'Invalid secret environment. Use dev, staging, prod, or an explicit { slug } option.'
56
+ )
57
+ }
58
+
59
+ // NODE_ENV retains the conventional development/test fallback.
60
+ if (env?.NODE_ENV === 'production' || env?.NODE_ENV === 'prod') {
61
+ return 'prod'
62
+ }
63
+
64
+ if (env?.NODE_ENV === 'staging') {
65
+ return 'staging'
66
+ }
67
+
68
+ return 'dev'
69
+ }
@@ -0,0 +1,29 @@
1
+ export type SecretErrorCode =
2
+ | 'CONFIGURATION'
3
+ | 'AUTHENTICATION'
4
+ | 'HTTP'
5
+ | 'CLI'
6
+ | 'VALIDATION'
7
+ | 'MISSING_SECRETS'
8
+ | 'TIMEOUT'
9
+ | 'ABORTED'
10
+ | 'LOADER'
11
+
12
+ /** Contains only safe diagnostic metadata; never attach underlying errors. */
13
+ export class SecretLoadError extends Error {
14
+ readonly code: SecretErrorCode
15
+ readonly operation?: string
16
+ readonly status?: number
17
+
18
+ constructor(
19
+ code: SecretErrorCode,
20
+ message: string,
21
+ details: { operation?: string; status?: number } = {}
22
+ ) {
23
+ super(message)
24
+ this.name = 'SecretLoadError'
25
+ this.code = code
26
+ this.operation = details.operation
27
+ this.status = details.status
28
+ }
29
+ }
@@ -1,225 +1,94 @@
1
- import { colorize, createCoolLogger } from '../cool-console-log'
2
- import { DEFAULT_INFISICAL_SITE_URL, loadInfisicalSecrets } from './infisical'
3
- const logger = createCoolLogger()
4
-
5
- export type SecretEnvironment = 'dev' | 'staging' | 'prod'
6
- export type SecretEnvironmentOption =
7
- | SecretEnvironment
8
- | (() => SecretEnvironment)
9
-
10
- export type InfisicalDefineSecretSetOptions = {
11
- readonly projectId: string
12
- readonly siteUrl?: string
13
- readonly environment?: SecretEnvironmentOption
14
- readonly clientIdEnvKey?: string
15
- readonly clientSecretEnvKey?: string
16
- readonly loader?: undefined
17
- }
18
-
19
- export type CustomLoaderDefineSecretSetOptions<SecretKey extends string> = {
20
- readonly loader: SecretSetLoader<SecretKey>
21
- readonly environment?: SecretEnvironmentOption
22
- }
23
-
24
- export type DefineSecretSetOptions<SecretKey extends string> =
25
- | InfisicalDefineSecretSetOptions
26
- | CustomLoaderDefineSecretSetOptions<SecretKey>
27
-
28
- export type SecretSetLoaderContext<SecretKey extends string> = {
29
- readonly keys: readonly SecretKey[]
30
- readonly environment: SecretEnvironment
31
- }
32
-
33
- export type SecretSetLoader<SecretKey extends string> = (
34
- context: SecretSetLoaderContext<SecretKey>
35
- ) => Promise<Partial<Record<SecretKey, string>>>
36
-
37
- type ResolvedSecretSetOptions<SecretKey extends string> = {
38
- readonly loader: SecretSetLoader<SecretKey>
39
- readonly environment: SecretEnvironment
40
- }
41
-
42
- export type SecretSet<SecretKey extends string> = {
43
- secret: (key: SecretKey) => string
44
- secrets: () => Record<SecretKey, string>
45
- reload: () => Promise<SecretSet<SecretKey>>
46
- }
47
-
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'
48
24
  export * from './infisical'
49
25
  export * from './loaders'
50
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>
51
46
 
52
47
  export function getSecretEnvironment(): SecretEnvironment {
53
- const env = process.env.SECRETS_ENV || process.env.NODE_ENV
54
- if (env === 'production' || env === 'prod') return 'prod'
55
- if (env === 'staging') return 'staging'
56
- return 'dev'
48
+ return resolveSecretEnvironment(
49
+ undefined,
50
+ getProcessEnvironment()
51
+ ) as SecretEnvironment
57
52
  }
58
53
 
59
54
  export async function defineSecretSet<
60
- const SecretKeys extends readonly string[]
55
+ const Keys extends readonly string[],
56
+ Parsed = Record<Keys[number], string>
61
57
  >(
62
- keys: SecretKeys,
63
- options: DefineSecretSetOptions<SecretKeys[number]>
64
- ): Promise<SecretSet<SecretKeys[number]>> {
65
- type SecretKey = SecretKeys[number]
66
-
67
- const keySet = [...new Set(keys)] as SecretKey[]
68
- const resolvedOptions = resolveSecretSetOptions(options)
69
- let cachedSecrets: Record<SecretKey, string> | null = null
70
-
71
- const loadSecrets = async (force = false) => {
72
- const nextSecrets = await loadAndMergeSecrets({
73
- keySet,
74
- cachedSecrets,
75
- force,
76
- ...resolvedOptions
77
- })
78
- cachedSecrets = nextSecrets
79
- return nextSecrets
80
- }
81
-
82
- const secretSet = createSecretSet({
83
- getCachedSecrets: () => cachedSecrets,
84
- reload: loadSecrets
85
- })
86
-
87
- await loadSecrets()
88
- return secretSet
89
- }
90
-
91
- function resolveSecretSetOptions<SecretKey extends string>(
92
- options: DefineSecretSetOptions<SecretKey>
93
- ): ResolvedSecretSetOptions<SecretKey> {
94
- const environment = resolveEnvironment(options.environment)
95
- const loader: SecretSetLoader<SecretKey> = options.loader
96
- ? options.loader
97
- : (context) =>
98
- loadInfisicalSecrets({
99
- ...context,
100
- projectId: options.projectId,
101
- siteUrl: options.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
102
- clientIdEnvKey: options.clientIdEnvKey ?? 'INFISICAL_CLIENT_ID',
103
- clientSecretEnvKey:
104
- options.clientSecretEnvKey ?? 'INFISICAL_CLIENT_SECRET'
105
- })
106
-
107
- return {
108
- loader,
109
- environment
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
110
69
  }
111
- }
112
70
 
113
- async function loadAndMergeSecrets<SecretKey extends string>({
114
- keySet,
115
- cachedSecrets,
116
- loader,
117
- environment,
118
- force
119
- }: {
120
- readonly keySet: readonly SecretKey[]
121
- readonly cachedSecrets: Record<SecretKey, string> | null
122
- readonly loader: SecretSetLoader<SecretKey>
123
- readonly environment: SecretEnvironment
124
- readonly force: boolean
125
- }): Promise<Record<SecretKey, string>> {
126
- const processEnvSecrets = readProcessEnvSecrets(
127
- keySet,
128
- force ? cachedSecrets : null
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()
129
76
  )
130
- const keysMissingFromEnv = keySet.filter((key) => !processEnvSecrets[key])
131
-
132
- logSecretEnvironment(environment, keySet.length)
133
-
134
- const loaderKeys = force ? keySet : keysMissingFromEnv
135
- const loadedSecrets: Partial<Record<SecretKey, string>> =
136
- loaderKeys.length === 0
137
- ? {}
138
- : await loader({
139
- keys: loaderKeys,
140
- environment
141
- })
142
-
143
- const missingKeys = keySet.filter(
144
- (key) => !(processEnvSecrets[key] ?? loadedSecrets[key])
145
- )
146
- if (missingKeys.length > 0) {
147
- throw new Error(`Missing secrets: ${missingKeys.join(', ')}`)
148
- }
149
-
150
- const nextSecrets = Object.fromEntries(
151
- keySet.map((key) => [
152
- key,
153
- (processEnvSecrets[key] ?? loadedSecrets[key]) as string
154
- ])
155
- ) as Record<SecretKey, string>
156
-
157
- for (const [key, value] of Object.entries(nextSecrets)) {
158
- process.env[key] = value as string
159
- }
160
-
161
- return nextSecrets
162
- }
163
-
164
- function createSecretSet<SecretKey extends string>({
165
- getCachedSecrets,
166
- reload
167
- }: {
168
- readonly getCachedSecrets: () => Record<SecretKey, string> | null
169
- readonly reload: (force?: boolean) => Promise<Record<SecretKey, string>>
170
- }): SecretSet<SecretKey> {
171
- const secretSet: SecretSet<SecretKey> = {
172
- secret(key) {
173
- const value = getCachedSecrets()?.[key] ?? process.env[key]
174
- if (!value) {
175
- throw new Error(`Secret ${key} not found`)
176
- }
177
- return value
178
- },
179
- secrets() {
180
- const cachedSecrets = getCachedSecrets()
181
- if (!cachedSecrets) {
182
- throw new Error('Secrets not initialized.')
183
- }
184
- return { ...cachedSecrets }
185
- },
186
- async reload() {
187
- await reload(true)
188
- return secretSet
189
- }
190
- }
191
-
192
- return secretSet
193
77
  }
194
78
 
195
- function resolveEnvironment(
196
- environment?: SecretEnvironmentOption
197
- ): SecretEnvironment {
198
- if (typeof environment === 'function') return environment()
199
- return environment ?? getSecretEnvironment()
200
- }
201
-
202
- function logSecretEnvironment(
203
- environment: SecretEnvironment,
204
- secretCount: number
205
- ): void {
206
- logger.info(colorize('Loading secrets', 'cyan', 'bold'), {
207
- environment: colorize(environment, 'brightCyan', 'bold'),
208
- count: secretCount
209
- })
210
- }
211
-
212
- function readProcessEnvSecrets<SecretKey extends string>(
213
- keys: readonly SecretKey[],
214
- previousSecrets: Record<SecretKey, string> | null = null
215
- ): Partial<Record<SecretKey, string>> {
216
- // On forced reloads, skip cached values so the loader can refresh them.
217
- return Object.fromEntries(
218
- keys
219
- .filter((key) => {
220
- const value = process.env[key]
221
- return value && value !== previousSecrets?.[key]
222
- })
223
- .map((key) => [key, process.env[key] as string])
224
- ) 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
+ })
225
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
+ }