npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

grm-shared-library

v1.1.216

Published

Common code for GRM — shared types, enums, interfaces, DTOs and constants for the Clarion / Upesy backend NestJS microservices.

Readme

grm-shared-library

Common code for GRM — shared types, enums, interfaces, DTOs and constants for the Clarion / Upesy backend NestJS microservices.

Calendar dates

One definition of what a date is, because there used to be four and they had drifted apart.

import {
  ISO_CALENDAR_DATE_PATTERN,  // the one regex - never write another
  isCalendarDate,             // a real YYYY-MM-DD day; rejects 2026-02-30
  toCalendarDate,             // Date | string -> YYYY-MM-DD | undefined
  isPlausibleBirthDate,       // not future, within 120 years, NO minimum age
  calculateAge, isMinor,      // whole years, UTC throughout
  AGE_OF_MAJORITY_YEARS,      // 18 - for COPY ONLY, never a validator
  IsCalendarDate, IsBirthDate // class-validator decorators over the above
} from 'grm-shared-library';

Why it refuses things

A calendar date has no time, no zone and no offset: somebody born on 17 April 1990 was born on that day in Nairobi, in Kampala and in London alike. Every bug this module exists to prevent comes from treating it as a moment.

The old conversion did new Date(freeText) and then .toISOString(). Under the production podsownTZ=Africa/Nairobi`:

| input | old result | | |---|---|---| | 04/17/1990 | 1990-04-16 | a day early | | new Date(1990,3,17) | 1990-04-16 | a day early - and this is what a date picker hands you | | 17/04/1990 | undefined | the East African spelling, refused | | 1990-04-17 | 1990-04-17 | correct |

toCalendarDate now accepts exactly three things and refuses everything else:

  1. YYYY-MM-DD
  2. a full ISO-8601 instant in UTC (1990-04-17T00:00:00.000Z)
  3. a Date, read in UTC - so build one with Date.UTC, never new Date(y,m,d)

An offset timestamp is refused too: 1990-04-17T00:00:00+03:00 is 16 April in UTC, and only the sender knows which day they meant.

Free text is refused rather than guessed at. 04/17/1990 and 17/04/1990 are the same eight characters in two orders, this platform serves Kenya, Uganda and Tanzania where the second is the common spelling, and there is no reading of that string that is safe to infer. A refused date surfaces as a field somebody is asked to collect; a drifted one is a permanently wrong birthday at a partner that cannot delete a member, on the record a paramedic uses to size a drug dose.

No minimum age

isPlausibleBirthDate refuses a future date and one beyond a human lifespan, and imposes no lower bound. Dependants on a family emergency-cover plan are children by design - that is the whole point of registering them with a responder - so a minimum-age rule would refuse exactly the people the feature exists for. AGE_OF_MAJORITY_YEARS is there for wording, not for gating.

A decorator alone guarantees nothing

@IsCalendarDate / @IsBirthDate are real validators, but on this platform they are not enforcement on their own: clarion-authentication registers no global ValidationPipe, the spreadsheet importer builds plain objects a pipe never sees, and internal service-to-service calls bypass HTTP entirely. Any service that OWNS a date must re-check it in the service layer, the way UserService.updateCountry re-checks isSupportedCountryCode.

Postgres round trip

A type: date column returns a string through this stack, not a Date: pg parses OID 1082 into a local-midnight Date and TypeORM reads it back with local components, so the two cancel out. It is correct, but correct by cancellation - never pass a value from a date column into new Date().


Country & Currency (multi-country foundation)

Platform-level primitives for country-aware emergency response, newsfeed, notifications, billing, maps and admin. This is the shared source of truth so services stop hard-coding Kenya-specific values.

Supported countries (ISO 3166-1 alpha-2):

| Code | Country | Calling code | Currency | Symbol | Decimals | Timezone | Locale | |------|----------|--------------|----------|--------|----------|------------------------|---------| | KE | Kenya | +254 | KES | KSh | 2 | Africa/Nairobi | en-KE | | UG | Uganda | +256 | UGX | USh | 0 | Africa/Kampala | en-UG | | TZ | Tanzania | +255 | TZS | TSh | 0 | Africa/Dar_es_Salaam | en-TZ |

Supported currencies (ISO 4217): KES, UGX, TZS.

Importing

Everything is re-exported from the package root:

import {
  CountryCode,
  CurrencyCode,
  CallingCode,
  SupportedCountryConfig,
  SupportedCurrencyConfig,
  SUPPORTED_COUNTRIES,          // readonly SupportedCountryConfig[]
  SUPPORTED_COUNTRIES_BY_CODE,  // Record<CountryCode, SupportedCountryConfig>
  SUPPORTED_CURRENCIES,
  SUPPORTED_CURRENCIES_BY_CODE,
  DEFAULT_COUNTRY_CODE,         // CountryCode.KE
  DEFAULT_CURRENCY_CODE,        // CurrencyCode.KES
  CountryScopeDto,              // optional { countryCode?, currencyCode? } request scope
  getSupportedCountryConfig,
  getSupportedCurrencyConfig,
  getCurrencyForCountry,
  getCallingCodeForCountry,
  getCountryByCallingCode,
  isSupportedCountryCode,
  isSupportedCurrencyCode,
} from 'grm-shared-library';

getCurrencyForCountry(CountryCode.UG);    // CurrencyCode.UGX
getCallingCodeForCountry(CountryCode.TZ); // '+255'
isSupportedCountryCode('US');             // false

Backward compatibility

Kenya (KE / KES) is the default market. Wherever a country or currency is not explicit, fall back to DEFAULT_COUNTRY_CODE / DEFAULT_CURRENCY_CODE / DEFAULT_TIMEZONE / DEFAULT_LOCALE. Existing Kenyan users, packages, posts, incidents, OTPs, notifications and billing flows are unaffected.

Scope

This package provides the country/currency primitives only. Country detection (GPS / IP / phone-number inference) and country-aware business logic (phone login, newsfeed filtering, multi-currency billing, SMS routing, map filtering) are delivered in later phases and should build on top of these types.

Tests

npm test   # compiles src/modules/country and runs the country/currency suite

Organization notification settings

Clarion is multi-tenant, so what a customer sees in a notification belongs to the organization that serves them. modules/notification carries the contract for that, shared by clarion-notification (owner), the gateway (proxy) and clarion-authentication (emitter):

  • organizationId is the key. The four auth event payloads (CreateOTPPayloadDto, CreateUserRegisteredPayloadDto, PasswordResetPayloadDto, AccountDeletionRequestPayloadDto) gained an optional organizationId, and EmailSendRequest / SmsSendRequest carry one. appId stays on every payload, but only as the fallback for an emitter that has not been upgraded. The notification service validates with whitelist: true, so it must be on a version that declares the field or the field is stripped before anything reads it.
  • Branding resolves field by field - NotificationBrandingSource: the organization's profile, then its record in clarion-organization, then the legacy per-app defaults, then the platform. UpsertOrganizationNotificationBrandingDto turns "" into null, and null means inherit again, not blank.
  • Email senders are verified per provider (EmailSenderProviderVerification). A provider only sends from an address it has VERIFIED; otherwise that provider uses the platform sender and keeps the organization's display name and reply-to.
  • SMS senders are requested, then approved (NotificationSenderStatus). Only ACTIVE senders are used; everything else falls back to the country-aware default. validateSmsSenderId(type, value) is the one definition of a valid sender.
  • Providers describe themselves (SmsProviderDescriptor), like tracking providers do. SmsProviderName only reserves a code; the catalogue says what is usable.
  • Every send result records sender and senderSource (ORGANIZATION | PLATFORM_DEFAULT), so "why was this Upesy-branded?" is a query.

clarion-billing → clarion-notification (HTTP)

BillingEmailNotificationDto, BillingSmsNotificationDto, BillingPushNotificationDto and their result types are the contract for POST /billing/notifications/email | sms | push — the synchronous path clarion-billing uses when it wants to know a message left. clarion-billing's NotificationClientService types its requests from these; do not redeclare the fields there.

  • BillingEmailTemplate is a closed set. The route renders a template by name on another service's say-so, so an open string would let a caller render any template. A new value needs copy defaults in clarion-notification (api/billing).
  • Attachments go by reference (documents[].signedUrl), because billing PDFs are private and their signed URLs expire within the hour. signedUrl must be https, file names must be bare names, and a subject may not contain a line break.
  • Every list and string is bounded; billing-notification.spec.ts pins the limits.

Values that end up inside email CSS or headers are validated as such: colours are #RRGGBB only, logos are https only, and a sender display name may not contain a line break. notification-sender.spec.ts pins each of those.

Roles and permissions

This package is the authority on both. clarion-authentication seeds them into its database on boot; every other service reads them from here.

| File | Holds | |---|---| | role/enums/role.enum.ts | Roles - the role names. | | role/constants/roles-by-scope.ts | ORGANIZATION_ROLES / CONTROL_CENTRE_ROLES / MOBILE_ROLES - each role's scope. | | role/data/roles.data.ts | ROLES_DATA - the permissions each role is seeded with. | | permission/enums/permission-actions.enum.ts | PermissionActions - every action, as verb:resource. | | permission/data/permissions.data.ts | PERMISSIONS_DATA - the catalogue role management shows, grouped by PermissionsModule. |

WHAT a role may do and WHERE it may do it are separate. Permissions answer the first and are checked by the gateway's PermissionGuard (ANY-of). Scope answers the second and comes only from which list in roles-by-scope.ts the role is in: the gateway (AccessScopeService), clarion-authentication (AuthorizationService) and clarion-socket (tracking rooms) all read those lists, and downstream services confine a caller to the scope the gateway built. A role in no list gets no scope at all and every tenant-scoped call it makes fails closed.

That is how one job exists at two scopes. Organization:Response-Units-Admin and Control-Centre:Response-Units-Admin are seeded from the same permission list (RESPONSE_UNITS_ADMIN_PERMISSIONS) - all of the Response Unit and Tracking modules, plus read:control-centre and read:organization, which their own screens call - and differ only in the list they are in.

The Tracking permission module

clarion-response-tracking's routes used to be guarded by the Response Unit actions. Every mobile user holds read:response-unit, so every app user could read their organization's tracking provider connections, and nobody could be given tracking without being given the response units. They now have their own:

| Action | Covers | |---|---| | create / read / update / delete:tracking-provider-connection | The link to a tracking platform (Telox, Wialon). update also covers test, enable, disable, reconnect. | | create / read / update / delete:tracking-device-binding | Which provider unit is which response unit. read also covers the unmapped-units worklist; update, resolving / reopening an entry on it. | | read:tracking-position | Where units are now - the incident map and live tracking. | | read:tracking-history | Where a unit has been. Separate on purpose: a movement history is a record of somebody's working day. | | read:tracking-operations | The tracking operations dashboard. | | manage:tracking-operations | Reconciling the tracking service's copy of the response units (the dry run included). |

Who holds them: Owner, Admin, Control-Centre:Admin and both Response-Units-Admins - all twelve (everybody who could administer tracking before the split still can); Organization:User - the five read actions; Control-Centre:User and Responder - position and history; Mobile-User - none.

The Notification permission module

An organization's notification settings (clarion-notification) decide what its customers see: how emails are branded, the address they come from, and the sender ID its SMS arrive under. They have actions of their own rather than reusing update:organization, because a wrong logo or sender goes out on every OTP and alert.

| Action | Covers | |---|---| | read:notification-settings | Reading the settings, the effective (resolved) branding and the SMS provider catalogue. | | manage:notification-settings | Saving branding; adding, verifying and removing email senders; requesting SMS senders; sending a test email. | | approve:notification-sender | Moving an SMS sender to ACTIVE / SUSPENDED / REJECTED. |

Owner and Admin hold the first two. No role holds approve:notification-sender - only Super-Admin (*) passes it, and roles.spec.ts pins that. Activating a sender asserts that it is registered with the provider and the telco; an organization able to approve its own could send SMS under any name it typed.

Adding a role: every step fails silently on its own

  1. Here - the Roles value, a scope list, and a ROLES_DATA entry. roles.spec.ts fails if a scoped role is unseeded, a seeded role is not in the enum, or a role holds an action the catalogue does not list.

    The same spec holds the rule for actions: every PermissionActions value must be in PERMISSIONS_DATA, under exactly one module. An action outside the catalogue still works - the gateway matches on the string - but role management can neither show nor assign it, which is how the four zone actions went unnoticed for so long (they now have a Zone module). Role and module names are also pinned to 50 characters, the width of the columns clarion-authentication stores them in.

  2. clarion-authentication - users.roles is a Postgres ENUM array, so the label needs an ALTER TYPE "users_roles_enum" ADD VALUE IF NOT EXISTS migration. Without it the role is seeded, offered by the admin, and rejected at the INSERT.

  3. Every service that reads the role lists - clarion-api-gateway, clarion-authentication, clarion-socket - must INSTALL the new version (the lockfile decides, not the caret) and redeploy. The gateway also validates roles with @IsEnum(Roles).

  4. clarion-socket names the organization roles admitted to tracking rooms one by one; an organization role that should see the fleet has to be added there.

  5. clarion-admin shows menu entries by role NAME (requiresAnyRole in sidebar-data.ts), from clarion-shared-types' mirror of Roles.

Deploy clarion-authentication first and let it reseed. The gateway caches role permissions for five minutes, so a gateway that starts REQUIRING a new action should go out after that window - or the roles that were just granted it are refused until their cache entry expires.