node
@aice-lab/notification — Node/TypeScript implementation of the aice-notification delivery contract (email + sms).
git@gitlab.com:aice-lab/notification/node.git
Latest commit
1fd7e2fc ·
README
aice-notification — Node.js implementation
Node.js / TypeScript implementation of the aice-notification specification. A delivery-only notification framework with exactly two channels — email and sms — behind one small NotificationPort. Defines the port, request/response types, typed errors, a reference MockChannel, a conformance harness that runs the spec’s vector suite against any adapter implementation, first-party channel adapters (Brevo, SMTP, Novocom), and an HttpNotificationClient for consumers who prefer to call a hosted notification-server instead of embedding a channel directly.
This framework delivers a payload; it does not know what the payload means. No OTP issuance, no templating engine, no retries/queues — see ARCHITECTURE.md for what’s deliberately out of scope.
This package is distributed as @aice-lab/notification via the aice-lab GitLab npm registry (anonymous public read).
Install
Add the @aice-lab scope to your project’s .npmrc:
@aice-lab:registry=https://gitlab.com/api/v4/groups/aice-lab/-/packages/npm/
If you use pnpm v10+, also add the next line so --frozen-lockfile in CI can fetch the tarball from GitLab’s non-standard URL pattern (see pnpm/pnpm#10913):
lockfile-include-tarball-url=true
Then install:
npm install @aice-lab/notification
# or
pnpm add @aice-lab/notification
Requires Node.js ≥ 20.
Quickstart
Every consumer of this package talks to the same NotificationPort
(sendEmail/sendSms) regardless of which binding delivers the request. Two
bindings ship today:
1. Embedded channels (in-process, BYO provider credentials)
Compose your own channel instances — each holding its own provider
credentials — into a NotificationService, which implements the full port:
import {
NotificationService,
BrevoEmailChannel,
NovocomSmsChannel,
type NotificationPort,
} from '@aice-lab/notification';
const email = new BrevoEmailChannel({
apiKey: process.env.BREVO_API_KEY!,
sender: { name: 'Your Brand', email: 'no-reply@your-tenant.example' },
});
const sms = new NovocomSmsChannel({
baseUrl: process.env.NOVOCOM_BASE_URL!,
apiKey: process.env.NOVOCOM_API_KEY!,
clientId: process.env.NOVOCOM_CLIENT_ID!,
senderId: process.env.NOVOCOM_SENDER_ID!,
});
const notifications: NotificationPort = new NotificationService(email, sms);
await notifications.sendEmail({
to: 'user@example.com',
subject: 'Your link',
text: 'Open https://app.example/verify?t=...',
});
await notifications.sendSms({ to: '+8801712345678', message: 'Your code is 123456' });
2. HttpNotificationClient (hosted notification-server)
Same port, no local provider credentials — the client calls a hosted
notification-server (server-to-server only; never ship its API key to a
browser or mobile client):
import { HttpNotificationClient, type NotificationPort } from '@aice-lab/notification';
const notifications: NotificationPort = new HttpNotificationClient({
baseUrl: 'https://notification.your-tenant.example',
apiKey: process.env.NOTIFICATION_API_KEY!,
});
await notifications.sendEmail({
to: 'user@example.com',
subject: 'Your link',
text: 'Open https://app.example/verify?t=...',
});
await notifications.sendSms({ to: '+8801712345678', message: 'Your code is 123456' });
Both bindings validate the request before contacting the provider (or, for
HttpNotificationClient, surface the server’s validation result) and raise
typed errors:
InvalidRecipientError— recipient is not a valid E.164 phone (sms) or RFC-5321 mailbox (email).InvalidPayloadError— required payload field (subject/text/message) is missing or empty.RateLimitedError— provider asked the caller to back off; carriesretryAfterSeconds.ProviderUnavailableError— provider is down or unreachable.QuotaExceededError— daily/monthly send quota exhausted.UnauthorizedError— the caller’s credentials were rejected.InternalNotificationError— anything else; checkcause.
All extend NotificationError; NotificationError.name matches the spec error code (invalid_recipient, rate_limited, …), so callers can branch on .name without depending on class identity.
A bare recipient string (no channel specified) can be routed to the right channel with routeRecipient:
import { routeRecipient } from '@aice-lab/notification';
routeRecipient('user@example.com'); // 'email'
routeRecipient('+8801712345678'); // 'sms'
Status
| Capability | Shipped |
|---|---|
NotificationPort interface, typed request/response/error surface | yes |
assertEmailRequest/assertSmsRequest (E.164 phones, RFC-5321 mailboxes) | yes |
routeRecipient (bare-recipient → channel routing) | yes |
MockChannel reference adapter for tests | yes |
| Conformance runner for spec vectors | yes |
NotificationService (composes an EmailChannel + SmsChannel into the full port) | yes |
BrevoEmailChannel, SmtpEmailChannel, NovocomSmsChannel first-party adapters | yes |
HttpNotificationClient (hosted notification-server binding) | yes |
License
Source: FSL-1.1-Apache-2.0 (see LICENSE and LICENSE.FAQ.md).
Documentation
Full documentation: https://notification.aice-lab.org
Contributing
See CONTRIBUTING.md. All commits require DCO sign-off.
Reporting security issues
See SECURITY.md. Do not open public issues for vulnerabilities.
This is a snapshot generated from GitLab. For the live README, see the project page.