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

@cobre-npm/library-portal-core

v0.41.0

Published

Shared configurations and resources for Portal MFEs

Readme

@cobre-npm/library-portal-core

Shared catalogs, configuration and interfaces for Cobre's Portal MFEs (Vue 3 + TS). It is a static data library (no app runtime): account types, counterparties, countries, currencies, movement types, document types, etc., together with their translated labels.

  • No runtime dependencies (only devDependencies).
  • Published compiled to dist/ (CommonJS + .d.ts).
  • Managed with pnpm; built with plain tsc.

Table of contents


Install & import (subpaths)

Each domain is exposed as its own package subpath. Always import from the domain's subpath, not from the root:

import { getAccountTypes } from "@cobre-npm/library-portal-core/accounts"
import { getCurrencies }   from "@cobre-npm/library-portal-core/currencies"
import { getCountries }    from "@cobre-npm/library-portal-core/countries"

| Subpath | Contents | |---|---| | @cobre-npm/library-portal-core/accounts | Account providers, types and statuses + Account interfaces | | @cobre-npm/library-portal-core/counterparties | Counterparty types + Counterparty interfaces | | @cobre-npm/library-portal-core/countries | Countries (base) and ISO countries (dialing codes, etc.) | | @cobre-npm/library-portal-core/movements | Movement types and statuses | | @cobre-npm/library-portal-core/subclients | Subclient statuses + Subclient/SubclientPerson interfaces | | @cobre-npm/library-portal-core/documentTypes | Document types (definition + per-country applicability) | | @cobre-npm/library-portal-core/currencies | Currencies | | @cobre-npm/library-portal-core/transactions | Transaction types + interfaces | | @cobre-npm/library-portal-core/canonicalTransactions | Canonical transaction/money-movement detail interfaces | | @cobre-npm/library-portal-core/search | Search / aggregation interfaces | | @cobre-npm/library-portal-core/trustedSessions | Trusted session (OTP elevation window) interfaces + constants | | @cobre-npm/library-portal-core/lang | Supported locales (Locale, defaults) | | @cobre-npm/library-portal-core (root) | ⚠️ Legacy / deprecated — see below |


Architecture: screaming architecture

Each domain is a self-contained folder whose name screams the domain (not the file type). The reference pattern is a folder with:

src/<domain>/
  catalog.ts                     # source of truth: object keyed by code + derived types
  utils/<x>.utils.ts             # the domain's public API (functions)
  locales/{en,es-mex}.json       # translations (only when the label is i18n)
  constants/<domain>.constants.ts # plain exported constants (only when the domain has any)
  index.ts                       # barrel: exposes ONLY the public API

A domain that only needs standalone constants (no catalog, no i18n) still gets its own constants/ subfolder — e.g. trustedSessions/constants/trustedSessions.constants.ts exports TRUSTED_SESSION_TTL_SECONDS. Same rule as everything else here: the file name screams the domain, never constants.ts alone, and the barrel re-exports it (export * from "./constants/<domain>.constants").

When a domain groups several catalogs, each one lives in its subfolder (e.g. accounts/providers/ and accounts/types/), and the entity interfaces in <domain>/interfaces/.

The source of truth: catalog.ts

The catalog is an as const object keyed by code (not an array), and the types are derived from it:

// countries/base/catalog.ts
export const COUNTRIES = {
  col: { code: "col" },
  mex: { code: "mex" },
  // ...
} as const

export type Country = keyof typeof COUNTRIES            // "col" | "mex" | ...
export type CountryRecord = (typeof COUNTRIES)[Country]

Keying by code gives direct CATALOG[code] access without .find(), and the types never drift from the data.


The consumption model (the "shape A")

The mental rule is: the library behaves like an API that returns the source of truth ready to consume.

Each domain exposes one getX() function that returns the localized catalog: the same object keyed by code, but with the label already included. The raw (untranslated) catalog is an internal detail and is not exported — just like an API doesn't expose its untranslated table.

import { getAccountTypes } from "@cobre-npm/library-portal-core/accounts"

const types = getAccountTypes("es-mex")
//    ^ { ch: { code: "ch", geo: ["col"], label: "Ahorros" }, cc: {...}, ... }

types.ch.label            // "Ahorros"          → direct access by code
types.ch.geo              // ["col"]            → all metadata comes included
Object.values(types)      // [ {...}, {...} ]   → the list, when you need it

Every use case comes out of this, without the consumer having to walk internal structures:

| I need… | How | |---|---| | The label of a code | getX(lang)[code].label | | The full list (dropdown, table) | Object.values(getX(lang)) | | Filter by some property | Object.values(getX(lang)).filter(x => …) |

The library delivers the data; the consumer decides how to filter it. There are no getXByGeo-style helpers: since each record already carries its metadata (geo, type, etc.), filtering is a trivial .filter() on the consumer side.

That is changing, one domain at a time. Catalogs are growing records that only make sense in one flow (cny/eur, counterparty settlement only) — something the consumer cannot tell from the record alone, so "filter it yourself" quietly leaks them into every picker. Domains are gaining a queryX() that returns the list already narrowed, with a safe default.

currencies is the first: see currencies for the query rules, which every later queryX() will follow. transactions/types is the second, and the first i18n one: lang travels inside the query object (required there, no default) rather than as a positional argument — see transactions for what that changes. documentTypes is the third, and the first many-to-many one — see documentTypes for how the query applies to a catalog flattened by geo. Until a domain has one, .filter() is still the way — and it stays fine for one-off predicates even where queryX() exists.

Two classes of domain

| Class | When | Signature | Examples | |---|---|---|---| | With i18n | the label depends on the language | getX(lang) | accountTypes, counterpartyTypes, movementTypes, transactionTypes, countries, countriesISO, documentTypes | | Without i18n | the label is fixed (or there is none) | getX() | currencies (inline label), accountProviders |

currencies/providers don't take lang because there's no translation — the signature is honest.

Type convention

  • XOption = the catalog record + label (what getX(lang) returns in i18n domains). This is the type consumers use to type results.
  • XRecord = the raw record. In non-i18n domains (currencies/providers) it is the result type (the label is already inline); in i18n domains it is internal (only input to XOption) and is not exported.
  • X (e.g. AccountType, Currency) = the union of codes. Always public (for typing variables/params).

Domains and their API

accounts

import {
  getAccountTypes, getAccountProviders, getAccountStatuses, queryAccountStatuses,
  type AccountType, type AccountTypeOption,
  type AccountProvider, type AccountProviderRecord,
  type AccountStatus, type AccountStatusOption, type AccountStatusQuery,
  type Account, type AccountResponsePaginated, /* ... interfaces */
} from "@cobre-npm/library-portal-core/accounts"

getAccountTypes("en")                                  // Record<AccountType, AccountTypeOption>  → { ch: { code, geo, label }, ... }
getAccountProviders()                                  // Record<AccountProvider, AccountProviderRecord>  (no i18n)
getAccountStatuses("en").closed                        // { code: "closed", type: "neutral", label: "Inactive" }
queryAccountStatuses({ lang: "en", type: "error" })    // [ disconnected, failed ]  → the list a picker needs
  • types (i18n): getAccountTypes(lang); each record carries code, geo (Country[]) and label.
  • providers (no i18n): getAccountProviders().
  • status (i18n): getAccountStatuses(lang); account statuses (creating, connected, connecting, disconnected, failed, closed). queryAccountStatuses({ lang, ... }) returns the same catalog as a list, narrowed by code/type — same matching rules as queryCurrencies, except lang is required (i18n domain, no default) and lives inside the query object rather than as a separate argument.
  • interfaces: Account, AccountResponsePaginated, AccountMetadata, AccountConnectivity, ParamsGetAccounts, CreateAccountRequest, UpdateAccountRequest, AccountDropdownMapper, …

counterparties

import {
  getCounterpartyTypes,
  type CounterpartyType, type CounterpartyTypeOption,
  type Counterparty, CounterpartyVerificationStatus, /* ... */
} from "@cobre-npm/library-portal-core/counterparties"

getCounterpartyTypes("es-mex")  // Record<CounterpartyType, CounterpartyTypeOption>  ({ code, geo, label })
  • types (i18n): getCounterpartyTypes(lang).
  • interfaces: Counterparty, CounterpartyMetadata, CounterpartyBankDetails, CounterpartyPaymentInformation, CounterpartyVerificationStatus (enum), ParamsGetCounterparties, CreateCounterpartyRequest, …

countries

Groups two catalogs:

import {
  getCountries, type Country, type CountryOption,
  getCountriesISO, getIndicativesWithCodes, hasCountryISOFlag,
  type CountryISO, type CountryISOOption, type IndicativeWithCode,
} from "@cobre-npm/library-portal-core/countries"

getCountries("en").col.label            // "Colombia"
getCountriesISO("en").col               // { code:"col", isoCode:"COL", indicative:"+57", label:"Colombia" }
getIndicativesWithCodes("en")           // view-model for a phone dialing-code selector
hasCountryISOFlag("col")                // true — whether a flag asset is available
  • base (i18n): business countries (col, mex, usa, global, …).
  • iso (i18n): full ISO catalog (~249 countries) with isoCode and indicative.

movements

import {
  getMovementTypes, queryMovementTypes, type MovementType, type MovementTypeGroupLabel,
  type MovementTypeOption, type MovementTypeQuery,
  getMovementStatuses, queryMovementStatuses, type MovementStatus, type MovementStatusType,
  type MovementStatusOption, type MovementStatusQuery,
} from "@cobre-npm/library-portal-core/movements"

getMovementTypes("en").ach.label                    // "ACH"
getMovementTypes("en").on_ramp.groupLabel            // "Stablecoin"
getMovementTypes("en").on_ramp.label                 // "On Ramp" (individual label, unchanged)
queryMovementTypes({ lang: "en", groupLabel: "Stablecoin" })  // global, off_ramp, on_ramp, stable_payout
getMovementStatuses("en").completed             // { code: "completed", label: "Completed", type: "success", icon: "icon-check" }
queryMovementStatuses({ lang: "en", type: "error" })  // error, failed, rejected — localized
  • types (i18n): movement types (ACH, SPEI, R2P, …). on_ramp/off_ramp/stable_payout/global additionally carry groupLabel: "Stablecoin" — a display grouping for views that need to show them under one label. getMovementTypes() keeps returning each rail's own individual label unchanged; a consumer that wants the grouped label picks groupLabel ?? label. There's no separate getXLabel for this — the value already comes back inside getMovementTypes(), same as everything else in this catalog.

  • types query (i18n queryX()): queryMovementTypes({ lang, ...filters }) — same rules as queryTransactionTypes/queryDocumentTypes, no default field. groupLabel is the only filterable field besides code, and it's optional on the record: a key that is present always filters, so groupLabel: undefined matches the 19 rails that don't carry a group, not "no filter". An optional UI filter has to be spread in conditionally:

    queryMovementTypes({ lang, ...(group.value ? { groupLabel: group.value } : {}) })
  • status (i18n): movement statuses (initiated, processing, completed, …). Each record also carries type: MovementStatusType (success | warning | error | info | neutral | pending) — a Design-System-agnostic classification, not a UI component's prop value. Note it's pending, not status-pending: the latter is a ds-v3 CobreBadge variant name, and using it would tie this library to that Design System. Mapping type to an actual badge variant/CSS class is each consumer's job. icon is present on 9 of the 11 statuses (under_review and pending_funds have none) — unlike type, icon is coupled to the portal's current icon set (its values are that set's class names); a consumer without it should map its own icon from code/type instead.

  • status query (i18n queryX()): queryMovementStatuses({ lang, ...filters })code and type are the filterable fields; there's no label filter, same as every other queryX() here. type is the real use case: queryMovementStatuses({ lang, type: "error" }) returns error, failed, rejected, replacing a hand-maintained list of codes with a query.

subclients

import {
  getSubclientStatuses, type SubclientStatus, type SubclientStatusOption,
  type Subclient, type SubclientPerson,
} from "@cobre-npm/library-portal-core/subclients"

getSubclientStatuses("en").approved     // { code: "approved", label: "Enabled" }
  • status (i18n): getSubclientStatuses(lang); subclient statuses (processing, approved, rejected, failed).
  • interfaces: Subclient, SubclientPerson (shape shared by ubos and legal_representatives).

documentTypes — three views

It's the only domain with a many-to-many relationship (one document type applies in several countries with different usage), so it exposes three ways to read the same source:

import {
  getDocumentTypes, getDocumentTypesByGeo, queryDocumentTypes,
  type DocumentType, type DocumentTypeUsage, type DocumentTypeGeo, type DocumentTypeValidationType,
  type DocumentTypeOption, type DocumentTypeGeoOption, type DocumentTypeQuery,
} from "@cobre-npm/library-portal-core/documentTypes"

// 1) By code (definition): unique list, validation, label
const docs = getDocumentTypes("en")     // Record<DocumentType, DocumentTypeOption>
Object.values(docs)                     // unique list of documents
docs.nit.minLength                      // validation by code
docs.nit.label                          // "NIT"

// 2) Flattened by country (applicability): per-geo/usage dropdowns, unfiltered
getDocumentTypesByGeo("en")             // DocumentTypeGeoOption[]  → { code, ...validations, label, geo, usage }

// 3) Flattened by country, narrowed by query (i18n queryX()): the picker/table case
queryDocumentTypes({ lang: "en", geo: "col", usage: "business" })  // only col's business-usage documents

Why three: the by-code view covers "unique list / validation / label" (each code once); the flattened-by-country view covers the full onboarding dropdown ("which documents does Colombia require"), where a code repeats per country; queryDocumentTypes is that same flattened view pre-narrowed — the third domain, after currencies and transactions/types, to get an i18n queryX(). lang goes inside the query object, same rule as transactions, and there is no default field (unlike currencies' scope): queryDocumentTypes({ lang: "en" }) returns everything, localized.

As with every queryX(), a key that is present always filters — it's the absence of the key that doesn't. So queryDocumentTypes({ lang, geo: "col" }) narrows to Colombia, but queryDocumentTypes({ lang, geo: undefined }) returns [], not "no geo filter". An optional UI filter has to be spread in conditionally:

queryDocumentTypes({ lang, ...(geo.value ? { geo: geo.value } : {}) })

getDocumentTypesByGeo("en").filter(d => d.geo === "col" && d.usage.includes("business")) still works — it's just what queryDocumentTypes({ lang: "en", geo: "col", usage: "business" }) now does for you.

The per-geo applicability lives inside DOCUMENT_TYPES[code].geo (a Partial<Record<DocumentTypeGeo, DocumentTypeUsage[]>>), not in a separate catalog — one source, and the same code can carry different usage per geo (nit is business-only in col but business-or-individual in usa). queryDocumentTypes filters the geo-flattened, unlabeled rows first and only attaches label to what matched, so DocumentTypeQuery never accepts label as a filter — unlike the other two XQuery types, this is enforced at the type level, not just by convention.

currencies

import { getCurrencies, queryCurrencies, type Currency, type CurrencyType, type CurrencyRecord,
  type CurrencyScope, type CurrencyQuery } from "@cobre-npm/library-portal-core/currencies"

getCurrencies().cop                     // { code:"cop", label:"COP", isoCode:"COP", geo:"col", flagAsset:"col", minAmount:1, type:"fiat" }
queryCurrencies({ type: "fiat" })       // [ cop, mxn, usd ]  → the list a picker needs

No lang: the label is fixed (a display code, the same in any language).

Which one to use. getCurrencies() is the catalog keyed by code — use it to look a single currency up (getCurrencies()[code]) or when you genuinely want all of them. queryCurrencies() returns a list, narrowed by the query, and is what a dropdown, table filter or picker should call.

Query rules

queryCurrencies()                                          // cop, mxn, usd, usd_stable, usdt, usdc, copco
queryCurrencies({ type: "fiat" })                          // cop, mxn, usd
queryCurrencies({ type: ["fiat", "stable"] })              // cop, mxn, usd, usd_stable, copco
queryCurrencies({ type: "fiat", geo: ["col", "mex"] })     // cop, mxn
queryCurrencies({ isoCode: "USD" })                        // usd, usd_stable, usdt, usdc
queryCurrencies({ scope: "counterparties" })               // cny, eur
  1. Any field of the record is queryable, by equality. AND across fields, OR within a field, so { type: ["fiat","stable"], geo: "col" } reads as (fiat or stable) and geo col. There are no operators — for a range or a substring, .filter() the result.

  2. A key that is present always filters. What does not filter is the absence of the key, so queryCurrencies({}) returns everything while queryCurrencies({ code: undefined }) returns nothing. An optional UI filter therefore has to be spread in conditionally:

    queryCurrencies({ type: "fiat", ...(country.value ? { geo: country.value } : {}) })

    This fails loudly — an empty dropdown — instead of silently returning more rows than intended.

  3. scope is the one field with a default of its own. Leave the key out and you get only the general-purpose currencies; ask for a scope and you get exactly the ones restricted to it. That is what keeps a new picker from inheriting cny/eur, which exist solely for counterparty settlement.

Watch out for two consequences of rule 3:

  • queryCurrencies() and queryCurrencies({ scope: "counterparties" }) are disjoint. For every currency a counterparty flow accepts, use Object.values(getCurrencies()).
  • queryCurrencies({ code: "eur" }) returns [], because the scope default drops eur before code is considered. Looking a currency up by code is getCurrencies().eur.

And two on rule 1: { minAmount: 1 } is equality, not >=, which is rarely what you want; and flagAsset/label are presentation details that make poor business criteria.

transactions

import {
  getTransactionTypes, queryTransactionTypes, type TransactionType, type TransactionTypeQuery,
  type Transaction, type EnrichedTransaction, /* ...interfaces */
} from "@cobre-npm/library-portal-core/transactions"

getTransactionTypes("en").spei_debit.label            // "Debit via SPEI"
queryTransactionTypes({ lang: "en", code: "spei_debit" })  // [ { code: "spei_debit", label: "Debit via SPEI" } ]
  • types (i18n): getTransactionTypes(lang); each record carries code, creditDebitType ("credit" | "debit" — the same CreditDebitType used by the Transaction interface) and label.
  • types query (i18n queryX()): queryTransactionTypes({ lang, ...filters })lang is required inside the query object, not a separate argument, so the result is already localized. There is no default field (unlike currencies' scope): every transaction type comes back when only lang is given. queryTransactionTypes({ lang: "en", creditDebitType: "credit" }) narrows to only the credits.
  • interfaces: Transaction, EnrichedTransaction, TransactionMetadata, CreditDebitType.

search / trustedSessions

Interfaces only (pure types):

import type { SearchRequest, SearchResponse, SearchFilter } from "@cobre-npm/library-portal-core/search"
import type { TrustedSessionStatus } from "@cobre-npm/library-portal-core/trustedSessions"

TrustedSessionStatus is the contract of /trusted-sessions: the OTP elevation window that lets a caller stop attaching an otp_token per request while the window is active.

trustedSessions also exports a plain constant, TRUSTED_SESSION_TTL_SECONDS, from constants/trustedSessions.constants.ts — the pattern for a domain that needs standalone constants (no catalog, no i18n): a constants/<domain>.constants.ts file, re-exported from the barrel.

canonicalTransactions

Interfaces only (pure types). This is the single source of truth for the canonical money-movement / transaction detail — built by core-portal-bff's transaction-canonical BFF, consumed as-is by the portal MFE (CanonicalMoneyMovement), and narrowed with Pick by the receipts-generation service:

import type {
  ICanonicalTransactionDetail, ICanonicalReceiptRequest,
  ICanonicalParticipant, ICanonicalSummary, ICanonicalAlert, /* ...all sections */
} from "@cobre-npm/library-portal-core/canonicalTransactions"
  • ICanonicalTransactionDetail is the full canonical shape (summary, classification, participants, references, timeline, forex_quote, errors, allowed_actions, approvals, balance, associated_transactions, split_information, derived, ...).
  • ICanonicalReceiptRequest is the wire contract core-portal-bff sends to the receipts-generation service ({ canonical, document_type, resource_id, locale?, timezone? }).
  • Fields reuse existing catalogs where they represent one (Currency, Country, CounterpartyType, DocumentType, MovementStatus) instead of a loose string.
  • This union was assembled by comparing the shape as it existed, slightly diverged, in three consumers (portal, core-portal-bff, util-portal-receipts) — see ICanonicalApprovalEvent's doc comment for one case that's a superset of two non-overlapping shapes rather than a clean merge, and revisit it once the real payload is confirmed.

i18n / languages

Translations live in locales/{en,es-mex}.json files inside each domain (never inline in code, except currencies, whose label is not translatable). The supported locales are typed:

import { type Locale, LOCALES, DEFAULT_ES_LOCALE, DEFAULT_EN_LOCALE }
  from "@cobre-npm/library-portal-core/lang"

// LOCALES              → ["en", "es-mex"]
// type Locale          → "en" | "es-mex"
// DEFAULT_ES_LOCALE    → "es-mex"

The getX(lang) functions take lang: Locale (strongly typed): passing an unsupported language is a compile-time error, not a silent fallback.


Legacy code (.) and its deprecation

The package root (@cobre-npm/library-portal-core, resolving to src/constants/*, src/interfaces/* and src/utils/amount.utils) is the previous version of these catalogs. All of its contents are marked @deprecated, pointing to their new domain:

/** @deprecated Use the `accounts` domain instead: `@cobre-npm/library-portal-core/accounts`. */
export const AccountProviders = { /* ... */ }

Why it was deprecated (not removed)

This migration to screaming architecture was done in a strictly additive way: the new domains were created without touching the old code, so that nothing the root currently exports stops working. The deprecation:

  • Breaks no one. @deprecated is just a hint (the IDE strikes it through and suggests the replacement); current consumers keep compiling and running the same.
  • Sets the direction. It signals that the source of truth moved to the new domains and that the old one will be retired.
  • Allows gradual migration. Moving each MFE (portal, portal-widgets, portal-checkout, core-portal-bff) to the new API is a separate, incremental task; meanwhile, both versions coexist without conflict.

Key differences of the new model vs the old one

  • The catalog moved from an array (or name→code map) to an object keyed by code with derived types.
  • The label is always delivered inside the list (getX(lang) returns records + label); there is no more getXLabel in i18n domains (the lookup is getX(lang)[code].label).
  • Filter helpers were removed (getXByGeo, etc.): the consumer filters with .filter() over the list, which already carries all the metadata.
  • Each domain's public API was reduced to the essentials: the code type + getX() + the result type. The raw catalogs and intermediate types were made internal.

Out of scope for this migration: utils/amount.utils (the formatAmount formatter) has no new replacement and is kept as-is at the root, not deprecated.


Development

Requires pnpm.

pnpm install
pnpm test                # vitest (tests colocated as *.test.ts next to the code)
pnpm exec tsc --noEmit   # type-check
pnpm lint                # eslint --fix
pnpm build               # tsc → dist/

publish:prod runs lint → type-check → build → publish.

Conventions

  • Strict TypeScript (strict + noUncheckedIndexedAccess), CommonJS, relative and extensionless imports.
  • Style (ESLint, no Prettier): 2 spaces, double quotes, no semicolons; comments with // (multi-line block /** */ only when documenting API); blank line before return/function.
  • Colocated tests (*.test.ts), Vitest, describe/it.
  • A new domain = self-contained folder (catalog.ts + utils/ + locales/ + index.ts) + an exports entry in package.json pointing to dist/<domain>/index.js. The index.ts exposes only the public API (code type
    • getX() + result type); the raw catalog stays internal.