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

@geonosis/integrations

v3.0.0

Published

One integration seam: a prefixed id, an optional settings schema, one registry that refuses a duplicate, a wrong prefix or a missing required id — and settings fields derived from the schema rather than hand-mapped.

Readme

@geonosis/integrations

One integration seam: a prefixed id, an optional settings schema, and a registry that refuses a duplicate id, an id outside its prefix, or a missing required id — at build time, once, instead of on every lookup for the life of the process.

Zero runtime dependencies. It never imports zod.

pnpm add @geonosis/integrations

Why

A Medusa storefront arrived at the same seam three times, with no shared code between them: AbstractCrmProvider + buildCrmProviderRegistry (cp_*), AbstractSearchProvider + buildSearchProviderRegistry (sp_*), and AbstractMessagingChannelProvider + buildMessagingProviderRegistry (mp_*). Midday is the same story one stage later: five incompatible plugin seams for one concept, a UnifiedApp manifest with 25 optional fields and value: any, a settings UI hand-mapped inside a dashboard component, and 8 of 35 integration directories reachable from nothing.

The measured gain is smaller and sharper than "one shape for all of them". A Medusa storefront declares

export const CRM_PROVIDER_PREFIX = 'cp_'      // modules/crm/types.ts:145
export const SEARCH_PROVIDER_PREFIX = 'sp_'   // modules/search/types.ts:135

and references neither anywhere else in the repo. Both registries are a Record<string, Provider> built from an object literal, so:

  • a key that ignores the prefix is accepted;
  • a second entry under an id already taken silently replaces the first, and the one that disappears is whichever was written higher up;
  • "the configured default must be registered" is checked inside getActiveProvider() — on every call, forever — instead of once, when the registry is built.

Two dead constants and three invariants stated in comments and held by nothing. This package holds them.

The boundary

Stated here because a Workers + D1 app closed this package not applicable and gave the reason as "the registry only models a user-installable integration" — which was a fact about a boundary nobody had written down. Every line below is measured in docs/ports-events-inventory-2026-08-30.md, which read all three a Medusa storefront seams and a Workers + D1 app's app catalogue before a line of this was written.

One registry, many doors. A door is an Integration<Id, Instance>: a prefixed id, a typed instance, and at most two probes. A registry is a set of doors behind one prefix, validated once when it is built. Id is the literal so crm.get('cp_attio') is checked rather than hoped, and Instance is one type for the whole registry — that is the abstract provider a seam is already written against, not a union of nine shapes.

What a door owns

| owned | why | |---|---| | its id, inside the registry's prefix | the prefix constants existed in a Medusa storefront and were referenced by nothing; here they are enforced | | its instance — whatever the consumer's own contract is | upsertContact, search, parseInbound are the consumer's nine methods and this package never sees them | | health() and isConfigured() | the two probes AbstractCrmProvider and AbstractSearchProvider both declare. AbstractMessagingChannelProvider declares neither, so both are optional | | a settingsSchema, structurally { safeParse } | so a settings form is derived. Never imported, never a dependency |

What a door may not own

| not owned | measured | |---|---| | a manifest — name, category, description, logo | zero occurrences across all three a Medusa storefront seams; Midday's has 25 optional fields and value: any. A catalogue is presentation, and it stays in the repo whose UI shows it | | an install lifecycle — installUrl, onCallback, onUninstall | zero occurrences in either consumer. A Medusa storefront's integrations are credential-configured; a Workers + D1 app's have no server half. The slot waits for a consumer that holds one | | storage — where settings, tokens or install state live | a registry that read a store could not be built at module scope, which is the one moment its three invariants can be checked | | the lookup key — a door is found by its id and by nothing else | buildMessagingProviderRegistry is keyed by CommunicationChannel and total with holes; see below | | resolution order or a default beyond requiredId | "the configured default must be registered" is answered once, when the registry is built, not on every getActiveProvider() call forever |

The doors, in full. defineIntegration builds one and isIntegration recognises one — the registry uses the second to refuse an entry that never went through the first, and it is exported because a consumer building its own list needs the same answer. createIntegrationRegistry takes a RegistryInput and hands back an IntegrationRegistry; defineIntegration takes an IntegrationInput and hands back an Integration. settingsFieldsOf turns a SchemaLike into SettingsFields. That is the whole surface, and src/boundary.test.ts fails by name if a door is added without a line here.

The one shape outside the boundary is the channel-keyed registry — Record<CommunicationChannel, Provider | undefined>, total with holes so a new channel breaks the build. It is a second registry shape, not a worse version of this one, and it is spelled out at the end of this file.

API

defineIntegration({ id, instance?, settingsSchema?, health?, isConfigured? })

import { defineIntegration } from '@geonosis/integrations'
import { z } from 'zod'

export const attio = defineIntegration({
  id: 'cp_attio',
  settingsSchema: z.object({
    apiKey: z.string().describe('The Attio API key'),
    baseUrl: z.string().optional(),
  }),
  health: () => client.ping(),
  isConfigured: () => apiKey !== undefined,
})

Refuses an empty or non-string id, a settingsSchema with no safeParse, and a health or isConfigured that is not callable.

health and isConfigured are the two probes AbstractCrmProvider and AbstractSearchProvider both declare. Everything a provider does beyond them — upsertContact, search, parseInbound — is the consumer's own contract and stays there; this package holds the seam, not the work.

The posture on instances (decided 2026-08-30)

The kit validates the registry AND may hold the instance. A repo that keeps its instances elsewhere passes nothing, declares nothing, and compiles exactly as before.

instance is optional and typed. It exists because a Medusa storefront measured its getCrmProvider('cp_attio') against this package and found 15 parity checks, 10 divergences, and every one of the ten was this one missing slot: their seam hands back a provider with nine methods on it, and a registry that validates entries and then makes the caller go and find the object somewhere else is a registry of names. Names were never the problem. The three build-time invariants — prefix, duplicate, required — are unchanged; this is additive.

const attio = defineIntegration({ id: 'cp_attio', instance: attioClient })

const crm = createIntegrationRegistry({ prefix: 'cp_', integrations: [attio, hubspot] })

crm.get('cp_attio').instance.upsertContact(…) // typed, all nine methods

The type parameter is inferred from the value, so nothing has to be written twice. It defaults to unknown: with no instance, entry.instance is undefined and its type is unknown, which is what this package actually knows about it. Every entry in one registry shares one Instance type — that is the abstract provider a seam is already written against.

createIntegrationRegistry({ prefix, integrations, requiredId? })

const crm = createIntegrationRegistry({
  prefix: 'cp_',
  integrations: [attio, hubspot],
  requiredId: 'cp_attio',
})

crm.get('cp_attio')   // throws, naming what IS registered, if absent
crm.has('cp_hubspot') // boolean, no throw
crm.list()            // declaration order
crm.required()        // the `requiredId` integration
crm.prefix            // 'cp_'

Refuses, when built: an empty prefix; an entry that never went through defineIntegration; an id outside the prefix; a duplicate id; a requiredId outside the prefix or absent from the list.

requiredId is a Medusa storefront's DEFAULT_CRM_PROVIDER_ID / DEFAULT_SEARCH_PROVIDER_ID plus the NOT_FOUND its getActiveProvider() throws — moved to the one moment the question can be answered once. (Plan 022 called this option manualImplementationRequired; it is named for the consumer construct it extracts.)

settingsFieldsOf(integration)

A JSON description of the settings schema's fields, so a settings form is derived rather than hand-mapped:

settingsFieldsOf(attio)
// [ { name: 'apiKey', type: 'string', required: true, description: 'The Attio API key' },
//   { name: 'baseUrl', type: 'string', required: false } ]

optional, default and prefault make a field not required; nullable and readonly are peeled without changing that; an enum carries its choices as options. An integration with no schema, or a schema that is not an object of fields, has no fields. A schema this reader cannot see into throws rather than returning an empty list — a blank settings page with no explanation is the silent zero this kit refuses everywhere else.

The one shape this package does not replace

A Medusa storefront's third seam is not this shape and is deliberately out of scope:

buildMessagingProviderRegistry(): Record<CommunicationChannel, AbstractMessagingChannelProvider | undefined>

It is keyed by CommunicationChannel, not by a prefixed id — the mp_* ids live in a separate MESSAGING_PROVIDER_IDS map the registry never uses — and it is total with holes: sms, telegram and webchat are present and undefined, so a new channel breaks the build until someone answers for it. Its own test asserts exactly that:

expect(reg.sms).toBeUndefined()   // providers.test.ts:157

A registry that refuses a non-integration destroys the totality that test protects. That is a second registry shape — keyed and total over a declared union — and replacing it with this one would be a downgrade. It is the only registry test either consumer has, which is why it is quoted here rather than summarised.

Migration — a Medusa storefront, five lines

-export function buildCrmProviderRegistry(opts: CrmProviderOptions = {}): Record<string, AbstractCrmProvider> {
-  return { [DEFAULT_CRM_PROVIDER_ID]: new AttioCrmProviderService(...), [CRM_PROVIDER_IDS.hubspot]: new HubspotCrmProviderService(...) }
-}
+export const buildCrmProviderRegistry = (opts: CrmProviderOptions = {}) =>
+  createIntegrationRegistry({
+    prefix: CRM_PROVIDER_PREFIX,
+    requiredId: DEFAULT_CRM_PROVIDER_ID,
+    integrations: [attioIntegration(opts), hubspotIntegration(opts)],
+  })

getActiveProvider() becomes registry.required(), and its NOT_FOUND throw is deleted — the registry already refused to exist. CRM_PROVIDER_PREFIX stops being a dead export.

Migration — a client-only catalogue, five lines

The shape the idiom is generalised from is a hardcoded array of 12 AppDefinitions ({ category, description, icon, id, name }) with client-only installedIds and no server half. Its first real integration is where the seam is worth having:

-const DEFAULT_APPS: AppDefinition[] = [ { id: 'slack', name: 'Slack', ... }, ... ]
+export const apps = createIntegrationRegistry({
+  prefix: 'app_',
+  integrations: [slack, gmail, outlook],
+})

The presentation fields (category, description, icon, name) stay in a Workers + D1 app: they are a UI catalogue, and this package deliberately carries no manifest. See below.

Equivalence status (D-027)

| Piece | Status | |---|---| | createIntegrationRegistry — prefix, duplicates, requiredId | extracted — three consumer copies, the invariants they state and do not hold | | defineIntegration — id, health, isConfigured | extracted — the fields two of the three abstractions declare | | settingsSchema + settingsFieldsOf | from Midday's failure, no consumer fixture yet — neither consumer holds a settings schema today; the field exists because value: any plus a hand-mapped dashboard component is where its absence leads | | manifest (name, category, description, logo) | not here — Midday's is 25 optional fields, and neither consumer's provider carries one | | install lifecycle (installUrl, onCallback, onUninstall) | not here — zero occurrences in either consumer. A Medusa storefront's integrations are credential-configured; a Workers + D1 app's have no server half. It waits for a consumer that holds one | | the channel-keyed registry | not replaced — see above |

Adopted by: nobody yet. This is a published package with contract tests over both consumers' measured shapes, not a migration that has happened.

zod, and why the guard is structural

settingsSchema is typed structurally — { safeParse } — and checked the same way. This package never imports zod, and has no runtime dependencies at all. The declared peer is ^4.0.0, optional.

The reason, measured rather than assumed:

| | a Medusa storefront plugins | a Medusa storefront root | a Workers + D1 app | |---|---|---|---| | import specifier | @medusajs/framework/zod | zod | zod (catalog) | | resolved version | 4.2.0 (via @medusajs/deps) | 4.4.3 | 4.5.4 |

Three module instances of one major, across two consumers, with genuinely different constructors and prototypes.

The obvious conclusion from that — "so an instanceof z.ZodType guard would refuse a schema built by another copy" — is wrong for zod 4, and this package's tests are where that was found out. Zod 4 puts a Symbol.hasInstance on its classes that answers by an internal trait rather than by the prototype chain, so instanceof bridges copies of zod 4 fine. It does not bridge zod 3, whose schemas are plain classes — and a Medusa storefront's own root package declares its zod peer as ^3.0.0 || ^4.0.0.

So the structural guard is still the right one, for the reasons that survived the measurement: it asks what a schema does, which every copy and every major answers identically; and it needs no import, which keeps this package out of the version disagreement entirely. settingsFieldsOf reads zod 4 internals and says so loudly when handed something else.

src/realms.test.ts asserts all of it against three real copies of zod (4.5, 4.2 and 3.25) in one process, starting with the assertion that they are genuinely different modules — without which the rest would pass and prove nothing.

Consumable from CommonJS

A Medusa backend is module: Node16 CommonJS by upstream requirement, not by preference. This package ships both builds and a require condition carrying its own .d.cts, so a static import type-checks and require() works:

import { defineIntegration } from '@geonosis/integrations'      // ESM, module: Node16 or ESNext
const { defineIntegration } = require('@geonosis/integrations') // CommonJS

An ESM-only package is a wall for such a backend, and no amount of parity gets over it: a Medusa storefront measured @geonosis/testbed at 15/15 identical and still could not adopt it, because a static import is TS1479 and await import() cannot serve the synchronous describe() registration its 107 suites are built on.

Licence

Apache-2.0