Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Publish

on:
release:
types: [published]
workflow_dispatch:

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # required for npm trusted publishing (OIDC) and provenance
steps:
- uses: actions/checkout@v4

- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: npm

# Trusted publishing needs npm >= 11.5.1; Node 22 ships with npm 10.
- name: Update npm
run: npm install -g npm@latest

- name: Install dependencies
run: npm ci

- name: Build all packages
run: npm run build --workspaces --if-present

- name: Lint
run: npm run lint

- name: Test
run: npm test

# Authenticates via npm trusted publishing (no token). Each package must have
# this repo + publish.yml configured as its Trusted Publisher on npmjs.com.
- name: Publish new versions
run: |
for dir in packages/*/; do
name=$(node -p "require('./${dir}package.json').name")
version=$(node -p "require('./${dir}package.json').version")
if npm view "${name}@${version}" version >/dev/null 2>&1; then
echo "${name}@${version} already published, skipping"
else
npm publish --workspace "$dir" --access public
fi
done
41 changes: 38 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,23 @@ This is an npm workspaces monorepo with three independent packages under

| Package | Providers | Spec section |
|---|---|---|
| [`email-service`](packages/email-service) | AWS SES, SendGrid, SMTP, Azure Communication Services | §1 |
| [`file-service`](packages/file-service) | AWS S3, Azure Blob Storage | §2 |
| [`feature-flag-management`](packages/feature-flag-management) | PostHog, LaunchDarkly, Unleash | §3 |
| [`@promact/email-service`](packages/email-service) | AWS SES, SendGrid, SMTP, Azure Communication Services | §1 |
| [`@promact/file-service`](packages/file-service) | AWS S3, Azure Blob Storage | §2 |
| [`@promact/feature-flag-management`](packages/feature-flag-management) | PostHog, LaunchDarkly, Unleash | §3 |

Each package exposes one provider-agnostic interface (`IEmailService`,
`IFileService<TFileModel>`, `IFeatureFlagService`); the concrete provider is
selected at wiring time via a `createXxxService(options)` factory. See each
package's own README/source for usage.

## Installation

```bash
npm install @promact/email-service
npm install @promact/file-service
npm install @promact/feature-flag-management
```

## Development

Requires Node.js 22+.
Expand All @@ -39,3 +47,30 @@ No real network/cloud calls are made in tests — provider SDKs are mocked.

`.github/workflows/ci.yml` runs `npm ci`, `npm run build`, `npm run lint`,
and `npm test` on every push and pull request against `master`.

## Publishing

Packages are published to npm under the `@promact` scope by
`.github/workflows/publish.yml`, which runs when a GitHub Release is published
(or manually via "Run workflow"). It builds, lints, and tests, then publishes
any package whose version isn't on npm yet. It authenticates with
[npm trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm
token is stored in the repo.

To release:

1. Bump the `version` of each package you changed (e.g.
`npm version patch -w @promact/email-service`) and merge to `master`.
2. Create a GitHub Release. Versions already on npm are skipped, so only
bumped packages are published.

### First-time setup

Trusted publishing is configured per package, so each package must exist on
npm before CI can publish it:

1. Publish the first version manually from a maintainer's machine:
`npm login`, then `npm run build && npm publish --workspaces`.
2. On npmjs.com, for each `@promact/*` package: **Settings → Trusted
Publisher → GitHub Actions**, with organization `Promact`, repository
`reusable-components-node`, and workflow `publish.yml`.
30 changes: 18 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion packages/email-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

Provider-agnostic email sending for Node.js/TypeScript — AWS SES, SendGrid, SMTP, and Azure Communication Services behind one `IEmailService` interface. Behavioral port of the .NET `nuget-packages/email-service` library; see `docs/specs/nuget-packages-spec.md` §1 for the full functional spec.

## Installation

```bash
npm install @promact/email-service
```

## Usage

```ts
import { createSesEmailService } from 'email-service';
import { createSesEmailService } from '@promact/email-service';

const emailService = createSesEmailService({ region: 'us-east-1' });

Expand Down
20 changes: 17 additions & 3 deletions packages/email-service/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
{
"name": "email-service",
"name": "@promact/email-service",
"version": "1.0.0",
"private": true,
"description": "Provider-agnostic email sending service (AWS SES, SendGrid, SMTP, Azure Communication Services)",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/Promact/reusable-components-node.git",
"directory": "packages/email-service"
},
"homepage": "https://github.com/Promact/reusable-components-node/tree/master/packages/email-service#readme",
"bugs": {
"url": "https://github.com/Promact/reusable-components-node/issues"
},
"publishConfig": {
"access": "public"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"prepublishOnly": "npm run build",
Expand Down
57 changes: 57 additions & 0 deletions packages/feature-flag-management/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# feature-flag-management

Provider-agnostic feature flag checks for Node.js/TypeScript — PostHog, LaunchDarkly, and Unleash behind one `IFeatureFlagService` interface. Behavioral port of the .NET `nuget-packages/feature-flag-management` library; see `docs/specs/nuget-packages-spec.md` §3 for the full functional spec.

## Installation

```bash
npm install @promact/feature-flag-management
```

## Usage

```ts
import { createLaunchDarklyFeatureFlagService } from '@promact/feature-flag-management';

const featureFlags = createLaunchDarklyFeatureFlagService({
sdkKey: process.env.LAUNCHDARKLY_SDK_KEY!,
environment: 'production',
userContextFactory: () => ({ userKey: currentUser.id, email: currentUser.email, subscription: 'Pro' }),
});

if (await featureFlags.isFeatureEnabled('new-checkout')) {
// ...
}
```

The other providers:

```ts
import {
createPostHogFeatureFlagService,
createUnleashFeatureFlagService,
} from '@promact/feature-flag-management';

const posthog = createPostHogFeatureFlagService({
apiKey: process.env.POSTHOG_API_KEY!,
host: 'https://us.posthog.com',
projectId: '12345',
});

const unleash = createUnleashFeatureFlagService({
appName: 'my-app',
unleashApi: 'https://unleash.example.com/api',
apiToken: process.env.UNLEASH_API_TOKEN!,
});
```

## Behavior

- `isFeatureEnabled` **never throws**: an unknown flag, a network failure, or an unreachable provider resolves to `false`. Missing required options, however, throw at construction time.
- The service only reads flags; it never creates or changes them.

## Provider notes

- **PostHog** checks the flag's global `active` state via the project's feature-flags API (`Authorization: Bearer {apiKey}`). No user context is sent, so percentage rollouts, cohorts, and multivariate flags are not evaluated. Every call is an HTTP request.
- **LaunchDarkly** evaluates locally against the SDK's cached ruleset. The user/subscription context comes from `userContextFactory`, read once per service instance; missing values default to `User` / `user@example.com` / `Basic`. Set `offline: true` to never contact LaunchDarkly (all flags evaluate to `false`).
- **Unleash** evaluates locally against the SDK's cached ruleset. `apiToken` is sent as the raw `Authorization` header (no `Bearer` prefix). `UnleashFeatureFlagService` also has `isFeatureForSubscriptionEnabled(name)`, which evaluates with a `Subscription` property from `subscriptionContextFactory` (default `Basic`). It isn't on the shared interface, so depend on the concrete class to use it.
20 changes: 17 additions & 3 deletions packages/feature-flag-management/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
{
"name": "feature-flag-management",
"name": "@promact/feature-flag-management",
"version": "1.0.0",
"private": true,
"description": "Provider-agnostic feature flag service (PostHog, LaunchDarkly, Unleash)",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/Promact/reusable-components-node.git",
"directory": "packages/feature-flag-management"
},
"homepage": "https://github.com/Promact/reusable-components-node/tree/master/packages/feature-flag-management#readme",
"bugs": {
"url": "https://github.com/Promact/reusable-components-node/issues"
},
"publishConfig": {
"access": "public"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"prepublishOnly": "npm run build",
Expand Down
54 changes: 54 additions & 0 deletions packages/file-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# file-service

Provider-agnostic file storage for Node.js/TypeScript — AWS S3 and Azure Blob Storage behind one `IFileService<TFileModel>` interface. Behavioral port of the .NET `nuget-packages/file-service` library; see `docs/specs/nuget-packages-spec.md` §2 for the full functional spec.

## Installation

```bash
npm install @promact/file-service
```

## Usage

```ts
import { createS3FileService, S3FileModel } from '@promact/file-service';

const fileService = createS3FileService({
accessKey: process.env.AWS_ACCESS_KEY_ID!,
secretKey: process.env.AWS_SECRET_ACCESS_KEY!,
region: 'us-east-1',
});

const file: S3FileModel = {
provider: 'S3',
bucketName: 'my-bucket',
keyName: 'reports/2026-10.pdf',
filePath: './reports/2026-10.pdf', // only needed for uploadFile
};

await fileService.uploadFile(file);
const url = await fileService.getSignedUrl(file, 3600);
const bytes = await fileService.getFileAsBytes(file);
const keys = await fileService.getKeys({ bucketOrContainer: 'my-bucket', pageNumber: 1, pageSize: 50 });
```

Azure Blob Storage works the same way with `createAzureBlobFileService({ connectionString })` and an `AzureFileModel` (`provider: 'Azure'`, `containerName`).

## API

| Method | Description |
|---|---|
| `uploadFile(model)` | Reads `model.filePath` from disk and uploads it, overwriting any existing object. |
| `getFileAsBytes(model)` | Downloads the object as a `Buffer`. |
| `deleteFile(model)` | Deletes the object; rejects with `FileNotFoundError` if it doesn't exist. |
| `getSignedUrl(model, expiresInSeconds)` | Returns a time-limited read URL (S3 presigned URL / Azure SAS URL); rejects with `FileNotFoundError` if it doesn't exist. |
| `checkFileExists(model)` | Resolves `true`/`false`. |
| `getKeys({ bucketOrContainer, pageNumber, pageSize })` | Lists object keys / blob names, 1-based paginated. |

## Provider notes

- Options are validated at construction time: S3 requires `accessKey`, `secretKey`, and a recognized AWS `region`; Azure requires `connectionString`. Invalid config throws `FileServiceConfigError`.
- Each adapter is typed to its own file model, and also checks it at runtime — passing an Azure model to the S3 service rejects with `FileModelTypeMismatchError`.
- `deleteFile` and `getSignedUrl` check existence first; `uploadFile` and `getFileAsBytes` don't, so a missing object in `getFileAsBytes` surfaces the provider's own "not found" error.
- `getKeys` pages by walking the provider's listing and skipping `(pageNumber - 1) * pageSize` items, so deep pages on large buckets make more provider calls.
- Exported error classes: `FileServiceConfigError`, `FileServiceValidationError`, `FileModelTypeMismatchError`, `FileNotFoundError`, `FileServiceOperationError` (wraps unexpected Azure failures, original error kept as `cause`).
Loading
Loading