From 0aefc4eb80261457ac2a095ffda345eed5e64a2e Mon Sep 17 00:00:00 2001 From: Roshni Shah Date: Mon, 5 Oct 2026 11:59:42 +0530 Subject: [PATCH 1/3] chore: prepare packages for npm publishing under @promact scope - Rename packages to @promact/email-service, @promact/file-service, @promact/feature-flag-management - Remove private flag, add license, repository and publishConfig - Add publish workflow triggered on GitHub Release - Document installation and release process in README Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/publish.yml | 48 +++++++++++++++++++ README.md | 28 +++++++++-- package-lock.json | 30 +++++++----- packages/email-service/README.md | 2 +- packages/email-service/package.json | 20 ++++++-- packages/feature-flag-management/package.json | 20 ++++++-- packages/file-service/package.json | 20 ++++++-- 7 files changed, 143 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..3e8620f --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,48 @@ +name: Publish + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # required for npm provenance + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 22.x + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: npm + registry-url: https://registry.npmjs.org + + - 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 + + - name: Publish new versions + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + 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" --provenance --access public + fi + done diff --git a/README.md b/README.md index 407665b..fc1aa26 100644 --- a/README.md +++ b/README.md @@ -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`, `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+. @@ -39,3 +47,17 @@ 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, tests, and then runs +`npm publish` for each package with provenance, using the `NPM_TOKEN` repo secret. + +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. diff --git a/package-lock.json b/package-lock.json index 8c8db97..a2ce955 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1938,6 +1938,18 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@promact/email-service": { + "resolved": "packages/email-service", + "link": true + }, + "node_modules/@promact/feature-flag-management": { + "resolved": "packages/feature-flag-management", + "link": true + }, + "node_modules/@promact/file-service": { + "resolved": "packages/file-service", + "link": true + }, "node_modules/@sendgrid/client": { "version": "8.1.6", "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.6.tgz", @@ -3324,10 +3336,6 @@ "dev": true, "license": "ISC" }, - "node_modules/email-service": { - "resolved": "packages/email-service", - "link": true - }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -3771,10 +3779,6 @@ "bser": "2.1.1" } }, - "node_modules/feature-flag-management": { - "resolved": "packages/feature-flag-management", - "link": true - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3788,10 +3792,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-service": { - "resolved": "packages/file-service", - "link": true - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -7099,7 +7099,9 @@ } }, "packages/email-service": { + "name": "@promact/email-service", "version": "1.0.0", + "license": "MIT", "dependencies": { "@aws-sdk/client-ses": "^3.716.0", "@azure/communication-email": "^1.0.0", @@ -7119,7 +7121,9 @@ } }, "packages/feature-flag-management": { + "name": "@promact/feature-flag-management", "version": "1.0.0", + "license": "MIT", "dependencies": { "@launchdarkly/node-server-sdk": "^9.13.1", "unleash-client": "^6.12.1" @@ -7133,7 +7137,9 @@ } }, "packages/file-service": { + "name": "@promact/file-service", "version": "1.0.0", + "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.700.0", "@aws-sdk/s3-request-presigner": "^3.700.0", diff --git a/packages/email-service/README.md b/packages/email-service/README.md index d1d9161..1d4c629 100644 --- a/packages/email-service/README.md +++ b/packages/email-service/README.md @@ -5,7 +5,7 @@ Provider-agnostic email sending for Node.js/TypeScript — AWS SES, SendGrid, SM ## Usage ```ts -import { createSesEmailService } from 'email-service'; +import { createSesEmailService } from '@promact/email-service'; const emailService = createSesEmailService({ region: 'us-east-1' }); diff --git a/packages/email-service/package.json b/packages/email-service/package.json index 808874f..cfa9d97 100644 --- a/packages/email-service/package.json +++ b/packages/email-service/package.json @@ -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", diff --git a/packages/feature-flag-management/package.json b/packages/feature-flag-management/package.json index e4a7608..1ce133b 100644 --- a/packages/feature-flag-management/package.json +++ b/packages/feature-flag-management/package.json @@ -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", diff --git a/packages/file-service/package.json b/packages/file-service/package.json index 1ed5bf5..d56ada7 100644 --- a/packages/file-service/package.json +++ b/packages/file-service/package.json @@ -1,11 +1,25 @@ { - "name": "file-service", + "name": "@promact/file-service", "version": "1.0.0", - "private": true, "description": "Provider-agnostic file storage service (AWS S3, Azure Blob Storage) — Node.js port of the .NET file-service nuget package", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/Promact/reusable-components-node.git", + "directory": "packages/file-service" + }, + "homepage": "https://github.com/Promact/reusable-components-node/tree/master/packages/file-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", From debcf9bdc8133e47d5952badf9344aace0d7116e Mon Sep 17 00:00:00 2001 From: Roshni Shah Date: Mon, 5 Oct 2026 12:09:18 +0530 Subject: [PATCH 2/3] docs: add READMEs for file-service and feature-flag-management, install steps for all packages Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/email-service/README.md | 6 +++ packages/feature-flag-management/README.md | 57 ++++++++++++++++++++++ packages/file-service/README.md | 54 ++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 packages/feature-flag-management/README.md create mode 100644 packages/file-service/README.md diff --git a/packages/email-service/README.md b/packages/email-service/README.md index 1d4c629..e8210bb 100644 --- a/packages/email-service/README.md +++ b/packages/email-service/README.md @@ -2,6 +2,12 @@ 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 diff --git a/packages/feature-flag-management/README.md b/packages/feature-flag-management/README.md new file mode 100644 index 0000000..25854b5 --- /dev/null +++ b/packages/feature-flag-management/README.md @@ -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. diff --git a/packages/file-service/README.md b/packages/file-service/README.md new file mode 100644 index 0000000..18dfacf --- /dev/null +++ b/packages/file-service/README.md @@ -0,0 +1,54 @@ +# file-service + +Provider-agnostic file storage for Node.js/TypeScript — AWS S3 and Azure Blob Storage behind one `IFileService` 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`). From 131dbaaf9b7b6f900fc96a1d4a12031830d605a7 Mon Sep 17 00:00:00 2001 From: Roshni Shah Date: Mon, 5 Oct 2026 12:17:09 +0530 Subject: [PATCH 3/3] ci: publish via npm trusted publishing instead of an NPM_TOKEN secret Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/publish.yml | 13 ++++++++----- README.md | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3e8620f..a458c29 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - id-token: write # required for npm provenance + id-token: write # required for npm trusted publishing (OIDC) and provenance steps: - uses: actions/checkout@v4 @@ -19,7 +19,10 @@ jobs: with: node-version: 22.x cache: npm - registry-url: https://registry.npmjs.org + + # 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 @@ -33,9 +36,9 @@ jobs: - 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 - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | for dir in packages/*/; do name=$(node -p "require('./${dir}package.json').name") @@ -43,6 +46,6 @@ jobs: if npm view "${name}@${version}" version >/dev/null 2>&1; then echo "${name}@${version} already published, skipping" else - npm publish --workspace "$dir" --provenance --access public + npm publish --workspace "$dir" --access public fi done diff --git a/README.md b/README.md index fc1aa26..2d9afd0 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,10 @@ and `npm test` on every push and pull request against `master`. 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, tests, and then runs -`npm publish` for each package with provenance, using the `NPM_TOKEN` repo secret. +(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: @@ -61,3 +63,14 @@ To release: `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`.