# jg-env

> Strict environment variable readers for process.env or import.meta.env, plus secret files, with errors that name the key and never the value.

- **Version:** `0.1.1`
- **Runtimes:** Universal
- **Install:** `npm install jg-env`
- **On npm:** [jg-env](https://www.npmjs.com/package/jg-env)
- **Source:** [packages/jg-env](https://github.com/jesusgraterol/jg-packages/tree/main/packages/jg-env)
- **Recipes:** [jg-env recipes](https://jg-packages.jesusgraterol.dev/packages/jg-env/recipes/index.md)

## Usage

Read a service's settings once, at startup, and fail fast when one is missing or malformed:

```ts
import { createEnvironmentReader } from 'jg-env';

const { readBoolean, readEnum, readInteger, readString } = createEnvironmentReader(process.env);

export const NODE_ENV = readEnum('NODE_ENV', ['development', 'production', 'test']);
export const PORT = readInteger('PORT'); // only plain decimal digits, so 1e3 and 0x10 fail
export const DATABASE_HOST = readString('DATABASE_HOST');
export const MAINTENANCE_MODE = readBoolean('MAINTENANCE_MODE'); // exactly true or false
```

The same readers take any record of variables, such as Vite's `import.meta.env`, whose built-in fields are booleans: only string values count, and anything else is invalid. A variable that's absent, empty, or blank is unset. `readOptionalString`, `readOptionalBoolean`, `readOptionalInteger`, and `readOptionalJson` return `undefined` for an unset variable instead of throwing.

Read JSON settings through a schema, such as a Zod schema:

```ts
import { createEnvironmentReader } from 'jg-env';
import { z } from 'zod';

const { readJson } = createEnvironmentReader(process.env);

export const ALLOWED_ORIGINS = readJson('ALLOWED_ORIGINS', z.array(z.url()));
```

### Secret files

`jg-env/node` reads secrets kept in files, such as Docker secrets. Inside a container, the secret's variable holds the path to its file; elsewhere, the file is `<KEY>.txt` in a secrets directory:

```ts
import { fileURLToPath } from 'node:url';

import { createSecretReader } from 'jg-env/node';
import { z } from 'zod';

const { readSecret, readSecretJson } = createSecretReader({
  isRunningInContainer: process.env['IS_RUNNING_IN_CONTAINER'] === 'true',
  secretsDirectoryPath: fileURLToPath(new URL('../secrets/', import.meta.url)),
});

export const DATABASE_PASSWORD = readSecret('DATABASE_PASSWORD'); // as it is, without trimming
export const ROOT_ACCOUNT = readSecretJson('ROOT_ACCOUNT', z.object({ email: z.email() }));
```

### Errors

| Code                         | When                                                                                                |
| ---------------------------- | --------------------------------------------------------------------------------------------------- |
| `ENV_VARIABLE_MISSING`       | A required variable is unset                                                                        |
| `ENV_VARIABLE_INVALID`       | A value isn't a string, an allowed value, `true` or `false`, a plain integer, JSON, or its schema's |
| `ENV_SECRET_FILE_UNREADABLE` | A secret's file is missing, unreadable, or empty                                                    |

An error's message and data name the variable, never its value. `ENV_VARIABLE_INVALID` has no `cause`, because a JSON parser's or a schema's error can quote the value. Inside a container, `ENV_SECRET_FILE_UNREADABLE` leaves out the path and the file system's error too, since a variable set to the secret itself by mistake would otherwise print it.

## <a id="api-reference"></a>jg-env API reference

Every export of `jg-env@0.1.1`, 8 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.

### <a id="api-entry-main"></a>`jg-env`

- Functions: [`createEnvironmentReader`](#api-createEnvironmentReader)
- Constants: [`ERRORS`](#api-ERRORS)
- Types: [`IEnvironmentReader`](#api-IEnvironmentReader), [`IEnvironmentSource`](#api-IEnvironmentSource), [`ISchema`](#api-ISchema)

#### <a id="api-functions"></a>Functions in `jg-env`

##### <a id="api-createEnvironmentReader"></a>`createEnvironmentReader`

*Function*

Creates the readers for a source of environment variables. Each read looks the variable up again, so it sees the source's current value. A variable is unset when the source doesn't hold it, or holds an empty or blank string; any other value that isn't a string is invalid. Errors name the variable and never its value.

```ts
createEnvironmentReader(source: IEnvironmentSource): IEnvironmentReader
```

**Parameters**

- `source` `IEnvironmentSource`: Where the variables come from, such as process.env or Vite's import.meta.env.

**Returns** `IEnvironmentReader`: The readers.

**Example**

```ts
import { createEnvironmentReader } from 'jg-env';

const { readEnum, readInteger, readString } = createEnvironmentReader({
  NODE_ENV: 'production',
  PORT: '8080',
  DATABASE_HOST: 'db.internal',
});

readString('DATABASE_HOST'); // 'db.internal'
readInteger('PORT'); // 8080
readEnum('NODE_ENV', ['development', 'production']); // 'production'
```

#### <a id="api-constants"></a>Constants in `jg-env`

##### <a id="api-ERRORS"></a>`ERRORS`

*Constant*

the codes for every error thrown by the package

```ts
const ERRORS: { ENV_VARIABLE_MISSING: "ENV_VARIABLE_MISSING"; ENV_VARIABLE_INVALID: "ENV_VARIABLE_INVALID"; ENV_SECRET_FILE_UNREADABLE: "ENV_SECRET_FILE_UNREADABLE" }
```

#### <a id="api-types"></a>Types in `jg-env`

##### <a id="api-IEnvironmentReader"></a>`IEnvironmentReader`

*Type*

reads a source's variables, each time from its current values. A required read of an unset variable throws ENV_VARIABLE_MISSING, and a value that can't be used throws ENV_VARIABLE_INVALID, without a cause, so neither ever holds the value

```ts
type IEnvironmentReader = {
    readString: (key: string) => string;
    readEnum: <const T extends string>(key: string, values: readonly T[]) => T;
    readBoolean: (key: string) => boolean;
    readInteger: (key: string) => number;
    readJson: <T>(key: string, schema: ISchema<T>) => T;
    readOptionalString: (key: string) => string | undefined;
    readOptionalBoolean: (key: string) => boolean | undefined;
    readOptionalInteger: (key: string) => number | undefined;
    readOptionalJson: <T>(key: string, schema: ISchema<T>) => T | undefined;
};
```

**Members**

- `readString: (key: string) => string` (property): reads a variable that must be set, as it is, without trimming
- `readEnum: (key: string, values: readonly T[]) => T` (property): reads a variable that must be one of the allowed values
- `readBoolean: (key: string) => boolean` (property): reads a variable that must be exactly true or false
- `readInteger: (key: string) => number` (property): reads a variable that must be an integer in plain decimal digits, such as 8080 or -2
- `readJson: (key: string, schema: ISchema<T>) => T` (property): reads a variable that must be JSON its schema accepts
- `readOptionalString: (key: string) => string | undefined` (property): the same as readString, but undefined when the variable is unset
- `readOptionalBoolean: (key: string) => boolean | undefined` (property): the same as readBoolean, but undefined when the variable is unset
- `readOptionalInteger: (key: string) => number | undefined` (property): the same as readInteger, but undefined when the variable is unset
- `readOptionalJson: (key: string, schema: ISchema<T>) => T | undefined` (property): the same as readJson, but undefined when the variable is unset

##### <a id="api-IEnvironmentSource"></a>`IEnvironmentSource`

*Type*

where the variables come from, such as process.env or Vite's import.meta.env. Only string values count as set, so a source may hold other values, such as import.meta.env's booleans

```ts
type IEnvironmentSource = Readonly<Record<string, unknown>>;
```

##### <a id="api-ISchema"></a>`ISchema`

*Type*

validates data and returns it typed, which a Zod schema does

```ts
type ISchema<T> = {
    parse(value: unknown): T;
};
```

**Members**

- `parse(value: unknown): T` (method)

### <a id="api-entry-node"></a>`jg-env/node`

- Functions: [`createSecretReader`](#api-node-createSecretReader)
- Types: [`ISecretReader`](#api-node-ISecretReader), [`ISecretReaderOptions`](#api-node-ISecretReaderOptions)

#### <a id="api-node-functions"></a>Functions in `jg-env/node`

##### <a id="api-node-createSecretReader"></a>`createSecretReader`

*Function*

Creates the readers for secrets kept in files, such as Docker secrets. Inside a container, each secret's variable holds the path to its file; elsewhere, the file is <KEY>.txt in the secrets directory. A file is read as it is, without trimming. Errors name the secret and never its value, and inside a container they leave out the path and the file system's error, which repeat whatever the variable holds.

```ts
createSecretReader(options: ISecretReaderOptions): ISecretReader
```

**Parameters**

- `options` `ISecretReaderOptions`: Whether the program runs in a container, and the secrets directory.

**Returns** `ISecretReader`: The readers.

**Example**

```ts
import { createSecretReader } from 'jg-env/node';

const { readSecret } = createSecretReader({
  isRunningInContainer: process.env['IS_RUNNING_IN_CONTAINER'] === 'true',
  secretsDirectoryPath: '/srv/app/secrets',
});

const databasePassword = readSecret('DATABASE_PASSWORD');
```

#### <a id="api-node-types"></a>Types in `jg-env/node`

##### <a id="api-node-ISecretReader"></a>`ISecretReader`

*Type*

reads secrets from files, at startup. A missing, unreadable, or empty file throws ENV_SECRET_FILE_UNREADABLE, and inside a container that error never names the path, since the path comes from a variable that might hold the secret itself by mistake

```ts
type ISecretReader = {
    readSecret: (key: string) => string;
    readSecretJson: <T>(key: string, schema: ISchema<T>) => T;
};
```

**Members**

- `readSecret: (key: string) => string` (property): reads a secret's file, as it is, without trimming
- `readSecretJson: (key: string, schema: ISchema<T>) => T` (property): reads a secret's file as JSON its schema accepts

##### <a id="api-node-ISecretReaderOptions"></a>`ISecretReaderOptions`

*Type*

where a secret reader finds each secret's file

```ts
type ISecretReaderOptions = {
    isRunningInContainer: boolean;
    secretsDirectoryPath: string;
};
```

**Members**

- `isRunningInContainer: boolean` (property): true where each secret's variable holds the path to its file, such as /run/secrets/DB_PASSWORD in a container
- `secretsDirectoryPath: string` (property): the directory that holds each secret as <KEY>.txt, used outside a container
