← All projects

node

@aice-lab/notification — Node/TypeScript implementation of the aice-notification delivery contract (email + sms).

  • TypeScript 98.98%
  • JavaScript 1.02%
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; carries retryAfterSeconds.
  • ProviderUnavailableError — provider is down or unreachable.
  • QuotaExceededError — daily/monthly send quota exhausted.
  • UnauthorizedError — the caller’s credentials were rejected.
  • InternalNotificationError — anything else; check cause.

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

CapabilityShipped
NotificationPort interface, typed request/response/error surfaceyes
assertEmailRequest/assertSmsRequest (E.164 phones, RFC-5321 mailboxes)yes
routeRecipient (bare-recipient → channel routing)yes
MockChannel reference adapter for testsyes
Conformance runner for spec vectorsyes
NotificationService (composes an EmailChannel + SmsChannel into the full port)yes
BrevoEmailChannel, SmtpEmailChannel, NovocomSmsChannel first-party adaptersyes
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.