Skip to content

Commit a06a2bb

Browse files
designcodeclaude
andauthored
fix(shared): resolve config on demand without mutating process.env (#183)
* fix(shared): resolve config on demand without mutating process.env Importing the server entry previously ran dotenv.config() as an import-time side effect, loading the consuming app's entire .env (including unrelated keys) into the global process.env. Remove the module-level exported config singleton in both storage and iam. Configuration is now resolved on demand via getConfig(), which each operation calls when it needs it. Env resolution parses .env into a private object (never process.env), keeps only TIGRIS_-prefixed keys, and prefers explicitly-set process.env values. Importing the SDK no longer has side effects, and apps that manage their own environment are no longer overridden. Closes #181 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a18ddbf commit a06a2bb

35 files changed

Lines changed: 242 additions & 61 deletions

.changeset/scoped-env-loading.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@tigrisdata/storage': patch
3+
'@tigrisdata/iam': patch
4+
---
5+
6+
Stop mutating the global `process.env` when loading configuration. Previously, importing the server entry ran `dotenv.config()` as an import-time side effect, loading the consuming app's entire `.env` (including unrelated keys) into `process.env`.
7+
8+
Configuration is now resolved on demand, per operation, directly from the environment: the SDK parses `.env` into a private object (never touching `process.env`), keeps only `TIGRIS_`-prefixed keys, and prefers explicitly-set `process.env` values. Importing the SDK no longer has side effects, and apps that manage their own environment are no longer overridden.

packages/iam/src/lib/config.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
1-
import { isNode, loadEnv } from '@shared/index';
1+
import { getEnvVar, isNode } from '@shared/index';
22
import type { TigrisIAMConfig } from './types';
33

44
export const DEFAULT_ENDPOINTS = {
55
iam: 'https://iam.storageapi.dev',
66
mgmt: 'https://mgmt.storageapi.dev',
77
};
88

9-
function loadIAMConfig(): TigrisIAMConfig {
10-
loadEnv();
11-
9+
/**
10+
* Resolve the Tigris IAM configuration from the environment, on demand.
11+
*
12+
* Reads only `TIGRIS_`-prefixed variables — from `process.env`, falling back to
13+
* a private parse of `.env` — and never mutates `process.env`. There is no
14+
* module-level config and importing this module has no side effects: each
15+
* operation calls `getConfig()` when it needs the current configuration.
16+
*/
17+
export function getConfig(): TigrisIAMConfig {
1218
const config: TigrisIAMConfig = {
1319
iamEndpoint: DEFAULT_ENDPOINTS.iam,
1420
mgmtEndpoint: DEFAULT_ENDPOINTS.mgmt,
1521
};
1622

1723
if (isNode()) {
18-
config.iamEndpoint = process.env.TIGRIS_IAM_ENDPOINT;
19-
config.mgmtEndpoint = process.env.TIGRIS_MGMT_ENDPOINT;
20-
config.sessionToken = process.env.TIGRIS_SESSION_TOKEN;
21-
config.organizationId = process.env.TIGRIS_ORGANIZATION_ID;
22-
config.accessKeyId = process.env.TIGRIS_STORAGE_ACCESS_KEY_ID;
23-
config.secretAccessKey = process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY;
24+
config.iamEndpoint = getEnvVar('TIGRIS_IAM_ENDPOINT');
25+
config.mgmtEndpoint = getEnvVar('TIGRIS_MGMT_ENDPOINT');
26+
config.sessionToken = getEnvVar('TIGRIS_SESSION_TOKEN');
27+
config.organizationId = getEnvVar('TIGRIS_ORGANIZATION_ID');
28+
config.accessKeyId = getEnvVar('TIGRIS_STORAGE_ACCESS_KEY_ID');
29+
config.secretAccessKey = getEnvVar('TIGRIS_STORAGE_SECRET_ACCESS_KEY');
2430
}
2531

2632
return config;
2733
}
28-
29-
export const config: TigrisIAMConfig = loadIAMConfig();

packages/iam/src/lib/http-client.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createTigrisHttpClient, type TigrisHttpClient } from '@shared/index';
2-
import { config, DEFAULT_ENDPOINTS } from './config';
2+
import { DEFAULT_ENDPOINTS, getConfig } from './config';
33
import type { TigrisIAMConfig, TigrisIAMResponse } from './types';
44

55
export const IAM_ENDPOINTS = {
@@ -38,17 +38,22 @@ export const IAM_ENDPOINTS = {
3838
};
3939

4040
function getIAMEndpoint(options?: TigrisIAMConfig): string {
41-
return options?.iamEndpoint ?? config.iamEndpoint ?? DEFAULT_ENDPOINTS.iam;
41+
return (
42+
options?.iamEndpoint ?? getConfig().iamEndpoint ?? DEFAULT_ENDPOINTS.iam
43+
);
4244
}
4345

4446
function getManagementEndpoint(options?: TigrisIAMConfig): string {
45-
return options?.mgmtEndpoint ?? config.mgmtEndpoint ?? DEFAULT_ENDPOINTS.mgmt;
47+
return (
48+
options?.mgmtEndpoint ?? getConfig().mgmtEndpoint ?? DEFAULT_ENDPOINTS.mgmt
49+
);
4650
}
4751

4852
export function createIAMClient(
4953
options?: TigrisIAMConfig,
5054
isManagement?: boolean
5155
): TigrisIAMResponse<TigrisHttpClient, Error> {
56+
const config = getConfig();
5257
const sessionToken = options?.sessionToken ?? config.sessionToken;
5358
const organizationId = options?.organizationId ?? config.organizationId;
5459
const accessKeyId = options?.accessKeyId ?? config.accessKeyId;

packages/storage/src/lib/bucket/snapshot.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from '@aws-sdk/client-s3';
66
import type { HttpRequest, HttpResponse } from '@aws-sdk/types';
77
import { TigrisHeaders } from '@shared/index';
8-
import { config } from '../config';
8+
import { getConfig } from '../config';
99
import { createTigrisClient } from '../tigris-client';
1010
import type { TigrisStorageConfig, TigrisStorageResponse } from '../types';
1111

@@ -37,6 +37,7 @@ export async function listBucketSnapshots(
3737
sourceBucketName?: string | ListBucketSnapshotsOptions,
3838
options?: ListBucketSnapshotsOptions
3939
): Promise<TigrisStorageResponse<ListBucketSnapshotsResponse, Error>> {
40+
const config = getConfig();
4041
if (typeof sourceBucketName === 'object') {
4142
options = sourceBucketName;
4243
sourceBucketName = undefined;
@@ -116,6 +117,7 @@ export async function createBucketSnapshot(
116117
sourceBucketName?: string | CreateBucketSnapshotOptions,
117118
options?: CreateBucketSnapshotOptions
118119
): Promise<TigrisStorageResponse<CreateBucketSnapshotResponse, Error>> {
120+
const config = getConfig();
119121
if (typeof sourceBucketName === 'object') {
120122
options = sourceBucketName;
121123
sourceBucketName = undefined;

packages/storage/src/lib/config.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
missingConfigError as baseMissingConfigError,
3+
getEnvVar,
34
isNode,
4-
loadEnv,
55
} from '@shared/index';
66
import type { TigrisStorageConfig } from './types';
77

@@ -17,24 +17,29 @@ const configMap: Partial<Record<keyof TigrisStorageConfig, string>> = {
1717
export const missingConfigError = (key: string) =>
1818
baseMissingConfigError(key, configMap[key as keyof TigrisStorageConfig]);
1919

20-
function loadStorageConfig(): TigrisStorageConfig {
21-
loadEnv();
22-
20+
/**
21+
* Resolve the Tigris storage configuration from the environment, on demand.
22+
*
23+
* Reads only `TIGRIS_`-prefixed variables — from `process.env`, falling back to
24+
* a private parse of `.env` — and never mutates `process.env`. There is no
25+
* module-level config and importing this module has no side effects: each
26+
* operation calls `getConfig()` when it needs the current configuration.
27+
*/
28+
export function getConfig(): TigrisStorageConfig {
2329
const config: TigrisStorageConfig = {
2430
endpoint: 'https://t3.storage.dev',
2531
};
2632

2733
if (isNode()) {
28-
config.bucket = process.env.TIGRIS_STORAGE_BUCKET ?? '';
29-
config.accessKeyId = process.env.TIGRIS_STORAGE_ACCESS_KEY_ID ?? '';
30-
config.secretAccessKey = process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY ?? '';
34+
config.bucket = getEnvVar('TIGRIS_STORAGE_BUCKET') ?? '';
35+
config.accessKeyId = getEnvVar('TIGRIS_STORAGE_ACCESS_KEY_ID') ?? '';
36+
config.secretAccessKey =
37+
getEnvVar('TIGRIS_STORAGE_SECRET_ACCESS_KEY') ?? '';
3138
config.endpoint =
32-
process.env.TIGRIS_STORAGE_ENDPOINT ?? 'https://t3.storage.dev';
33-
config.sessionToken = process.env.TIGRIS_SESSION_TOKEN;
34-
config.organizationId = process.env.TIGRIS_ORGANIZATION_ID;
39+
getEnvVar('TIGRIS_STORAGE_ENDPOINT') ?? 'https://t3.storage.dev';
40+
config.sessionToken = getEnvVar('TIGRIS_SESSION_TOKEN');
41+
config.organizationId = getEnvVar('TIGRIS_ORGANIZATION_ID');
3542
}
3643

3744
return config;
3845
}
39-
40-
export const config: TigrisStorageConfig = loadStorageConfig();

packages/storage/src/lib/fork/_legacy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { fetchBucketListing } from '../bucket/listing';
7-
import { config } from '../config';
7+
import { getConfig } from '../config';
88
import type { TigrisStorageResponse } from '../types';
99
import type { BucketFork, ListForksOptions, ListForksResponse } from './list';
1010

@@ -19,6 +19,7 @@ export async function listForksLegacy(
1919
sourceBucketName?: string | ListForksOptions,
2020
options?: ListForksOptions
2121
): Promise<TigrisStorageResponse<ListForksResponse, Error>> {
22+
const config = getConfig();
2223
if (typeof sourceBucketName === 'object') {
2324
options = sourceBucketName;
2425
sourceBucketName = undefined;

packages/storage/src/lib/fork/list.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { fetchBucketListing } from '../bucket/listing';
22
import type { Bucket } from '../bucket/types';
3-
import { config } from '../config';
3+
import { getConfig } from '../config';
44
import type { TigrisStorageConfig, TigrisStorageResponse } from '../types';
55
import { listForksLegacy } from './_legacy';
66

@@ -46,6 +46,7 @@ export async function listForks(
4646
sourceBucketName?: string | ListForksOptions,
4747
options?: ListForksOptions
4848
): Promise<TigrisStorageResponse<ListForksResponse, Error>> {
49+
const config = getConfig();
4950
if (typeof sourceBucketName === 'object') {
5051
options = sourceBucketName;
5152
sourceBucketName = undefined;

packages/storage/src/lib/http-client.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { createTigrisHttpClient, type TigrisHttpClient } from '@shared/index';
2-
import { config } from './config';
2+
import { getConfig } from './config';
33
import type { TigrisStorageConfig, TigrisStorageResponse } from './types';
44

55
function getStorageEndpoint(options?: TigrisStorageConfig): string {
6-
return options?.endpoint ?? config.endpoint ?? 'https://t3.storage.dev';
6+
return options?.endpoint ?? getConfig().endpoint ?? 'https://t3.storage.dev';
77
}
88

99
export function createStorageClient(
1010
options?: TigrisStorageConfig
1111
): TigrisStorageResponse<TigrisHttpClient, Error> {
12+
const config = getConfig();
1213
const sessionToken = options?.sessionToken ?? config.sessionToken;
1314
const organizationId = options?.organizationId ?? config.organizationId;
1415
const accessKeyId = options?.accessKeyId ?? config.accessKeyId;

packages/storage/src/lib/object/bundle.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TigrisHeaders, toError } from '@shared/index';
2-
import { config, missingConfigError } from '../config';
2+
import { getConfig, missingConfigError } from '../config';
33
import { createStorageClient } from '../http-client';
44
import type { TigrisStorageConfig, TigrisStorageResponse } from '../types';
55

@@ -50,6 +50,7 @@ export async function bundle(
5050
keys: string[],
5151
options?: BundleOptions
5252
): Promise<TigrisStorageResponse<BundleResponse, Error>> {
53+
const config = getConfig();
5354
const bucket = options?.config?.bucket ?? config.bucket;
5455

5556
if (!bucket) {

packages/storage/src/lib/object/copy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TigrisHeaders } from '@shared/headers';
22
import { encodeObjectKey, handleError } from '@shared/utils';
3-
import { config, missingConfigError } from '../config';
3+
import { getConfig, missingConfigError } from '../config';
44
import { createStorageClient } from '../http-client';
55
import type { TigrisStorageConfig, TigrisStorageResponse } from '../types';
66

@@ -31,6 +31,7 @@ export async function copyOrMove(
3131
rename: boolean,
3232
options?: CopyOptions
3333
): Promise<TigrisStorageResponse<CopyResponse, Error>> {
34+
const config = getConfig();
3435
if (!src || !dest) {
3536
return { error: new Error('src and dest are required') };
3637
}

0 commit comments

Comments
 (0)