npm package: @johnmmackey/app-toolkit
Description: A collection of shared utility functions
Latest version: 0.4.0

# app-toolkit

A collection of shared utility functions for common application tasks.

## Installation

```bash
npm install @johnmmackey/app-toolkit
```

## Usage

```ts
import {
  getLogger, setBaseLevel, setDebugModules,
  getAssumedRoleCredentials,
  HttpRouteHandlerError,
  SystemsManager,
} from '@johnmmackey/app-toolkit';
```

## Development

### Prerequisites

- Node.js >= 18
- npm >= 9

### Setup

```bash
npm install
```

### Scripts

| Command | Description |
|---|---|
| `npm run build` | Compile TypeScript to `dist/` |
| `npm run dev` | Build in watch mode |
| `npm test` | Run tests |
| `npm run test:watch` | Run tests in watch mode |
| `npm run typecheck` | Type-check without emitting |
| `npm run lint` | Lint source files |

### Project Structure

```
app-toolkit/
├── src/
│   ├── index.ts                 # Public API entry point
│   ├── logger.ts                # Winston logger utilities
│   ├── aws-role-credentials.ts  # STS role assumption
│   ├── SystemsManager.ts        # AWS SSM parameter store
│   ├── HttpRouteHandlerError.ts # HTTP-aware error class
│   └── types/
│       └── index.ts             # Shared type re-exports
├── dist/              # Compiled output (generated)
├── package.json
├── tsconfig.json
└── README.md
```

## API

### `getLogger(label: string): Logger`

Returns a [Winston](https://github.com/winstonjs/winston) child logger bound to the given module label.

Every log entry is emitted as structured JSON with a `timestamp` and `label` field, ready to be ingested by any log aggregator.

```ts
import { getLogger } from '@johnmmackey/app-toolkit';

const log = getLogger('my-module');

log.info('server started', { port: 3000 });
// → {"level":"info","label":"my-module","port":3000,"timestamp":"..."}

log.error('unexpected failure', new Error('oops'));
// → {"level":"error","label":"my-module","message":"oops","stack":"...","timestamp":"..."}
```

**Environment variables**

| Variable | Default | Description |
|---|---|---|
| `LOG_LEVEL` | `"info"` | Global log level (`error` \| `warn` \| `info` \| `debug`) |
| `APP_DEBUG_MODULES` | — | Space-separated list of [micromatch](https://github.com/micromatch/micromatch) glob patterns. Any logger whose `label` matches is promoted to `debug` level regardless of `LOG_LEVEL`. |

```bash
# promote only the "db" and "auth-*" loggers to debug
APP_DEBUG_MODULES="db auth-*" node dist/server.js
```

#### `setBaseLevel(level: string): void`

Changes the log level on all existing child loggers at runtime.

```ts
import { setBaseLevel } from '@johnmmackey/app-toolkit';

setBaseLevel('debug');
```

#### `setDebugModules(patterns: string[]): void`

Promotes any logger whose label matches one of the provided [micromatch](https://github.com/micromatch/micromatch) glob patterns to `debug` level.

```ts
import { setDebugModules } from '@johnmmackey/app-toolkit';

setDebugModules(['db', 'auth-*']);
```

---

### `getAssumedRoleCredentials(roleArn: string, region?: string): Promise<AwsCredentialIdentity>`

Assumes an AWS IAM role via STS and returns temporary credentials. Results are cached in-process and automatically refreshed 5 minutes before expiry.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `roleArn` | `string` | — | ARN of the role to assume (required) |
| `region` | `string` | `"us-east-1"` | AWS region for the STS API call |

```ts
import { getAssumedRoleCredentials } from '@johnmmackey/app-toolkit';

const creds = await getAssumedRoleCredentials(
  'arn:aws:iam::123456789012:role/MyRole',
  'eu-west-1',
);

// Pass credentials to any AWS SDK client
import { S3Client } from '@aws-sdk/client-s3';
const s3 = new S3Client({ credentials: creds, region: 'eu-west-1' });
```

or, since ```credentials``` can by an async function,

```ts
import { getAssumedRoleCredentials } from '@johnmmackey/app-toolkit';

// Pass credentials to any AWS SDK client
import { S3Client } from '@aws-sdk/client-s3';
const s3 = new S3Client({ credentials: async () => getAssumedRoleCredentials(arn)});
```

**Credential resolution (STS client)**
- **Dev**: relies on environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) or a shared credentials file.
- **Prod / ECS / Lambda**: relies on the instance / task metadata service (IMDSv2).

---

### `HttpRouteHandlerError`

An `Error` subclass that carries an HTTP status code. Intended to be caught by route-level error handlers.

```ts
import { HttpRouteHandlerError } from '@johnmmackey/app-toolkit';

throw new HttpRouteHandlerError('Not found', 404);
throw new HttpRouteHandlerError('Something went wrong'); // defaults to 500
```

| Property | Type | Description |
|---|---|---|
| `message` | `string` | Error description |
| `code` | `number` | HTTP status code (default `500`) |

---

### `SystemsManager`

A class that loads and caches application configuration from [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html). Parameters are fetched under the path `/{appName}/{envName}/` and can be overridden by environment variables of the same name.

#### `new SystemsManager(appName, envName, roleArn)`

| Parameter | Type | Description |
|---|---|---|
| `appName` | `string` | Application name (first path segment) |
| `envName` | `string` | Environment name, e.g. `"prod"` (second path segment) |
| `roleArn` | `string` | ARN of the IAM role to assume for SSM access |

```ts
import { SystemsManager } from '@johnmmackey/app-toolkit';

const config = new SystemsManager(
  'my-app',
  'prod',
  'arn:aws:iam::123456789012:role/SsmReadRole'
);
```

#### `config.loadSystemParameters(): Promise<void>`

Fetches all parameters for the configured app/env path. Must be called once at application startup before any `get*` method.

```ts
await config.loadSystemParameters();
```

#### `config.getSystemParameter(name): string`

Returns the string value of a named parameter. Throws if the parameter is a `StringList`.

```ts
const dbUrl = config.getSystemParameter('DATABASE_URL');
```

#### `config.getSystemParameterArray(name): string[]`

Returns the value of a `StringList` parameter as a `string[]`. Throws if the parameter is a plain string.

```ts
const allowedOrigins = config.getSystemParameterArray('ALLOWED_ORIGINS');
```

#### `config.loggableSystemParameters(): Record<string, SystemParameter>`

Returns a snapshot of all cached parameters safe for logging — `SecureString` values are masked, showing only the first and last 4 characters.

```ts
const log = getLogger('startup');
log.info('loaded config', config.loggableSystemParameters());
```

---

### Types

Named types used by this package are re-exported from the `/types` sub-path and also via the `AppToolkitTypes` namespace for convenience.

```ts
// Individual type imports
import type { AppLogger, AwsCredentialIdentity, SystemParameter } from '@johnmmackey/app-toolkit';

// Namespace import
import type { AppToolkitTypes } from '@johnmmackey/app-toolkit';
type MyLogger = AppToolkitTypes.AppLogger;
```

| Type | Source | Description |
|---|---|---|
| `AppLogger` | `winston` | Winston `Logger` instance type |
| `AwsCredentialIdentity` | `@aws-sdk/types` | AWS temporary credential shape |
| `SystemParameter` | `SystemsManager` | Cached SSM parameter shape (`value`, `fromEnvironment`, `secure`) |

## Contributing

1. Create a feature branch from `main`
2. Make your changes with tests
3. Run `npm test` and `npm run typecheck` to verify
4. Open a pull request

## Changelog

See [CHANGELOG.md](./CHANGELOG.md).

## License

MIT