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

@volter/twin-intercom

v0.1.35

Published

Local Intercom twin — a faithful, stateful local Intercom REST API your unmodified intercom-client talks to. Mirror, simulate, and fork. Built on @volter/world-core.

Readme

@volter/twin-intercom

The Intercom twin — a local, stateful, vendor-faithful replica of the Intercom REST API (api.intercom.io, Intercom-Version: 2.14) on the shared @volter/world-core kernel. An unmodified intercom-client points at it and gets vendor-correct responses.

State lives in the kernel's append-only action/event log — a write appends an action that projects into visible state; reads use the kernel’s tree. No real Intercom is ever called (the only network path is the connector, over an injected client). Auth is a faked-locally Bearer access token — accepted, never verified. There is no OAuth token minting and no webhook HMAC verification here: both are the app-credential surface, not the REST data surface (both are filed as todos).

Usage

bun run src/cli.ts serve            # the Intercom REST API twin (writable)
bun run src/cli.ts serve --read-only
bun run src/cli.ts mirror           # the React inbox mirror (conversations + contacts)
bun run src/cli.ts conformance      # offline spec check over projected state

Point the real SDK at it with the SDK's own public option — configuration, not modification:

import { IntercomClient } from 'intercom-client';
const client = new IntercomClient({ token: 'anything', baseUrl: 'http://127.0.0.1:<port>' });
await client.contacts.create({ email: '[email protected]', role: 'user' });

Responses use Intercom's real envelopes: lists are { "type": "list", "data": [...], "total_count": N, "pages": { "type": "pages", "page", "next", "per_page", "total_pages" } } (conversations use { "type": "conversation.list", "conversations": [...] }), and every error is { "type": "error.list", "request_id": "…", "errors": [{ "code", "message", "field"? }] }. Timestamps are unix seconds, never ISO strings. Pagination is Intercom's opaque starting_after cursor (per_page + starting_after, and pagination in a search body).

Coverage

The capability manifest (src/intercom-capabilities.ts) is the real Intercom surface as the denominator, enumerated top-down from Intercom's own first-party OpenAPI document (intercom/Intercom-OpenAPI, descriptions/2.14/api.intercom.io.yaml — 106 paths / 150 operations across 31 tagged areas, read 2026-08-19) — so coverage is honest and partial. Run bun scripts/manifest-baseline-one.ts intercom for the live count. As of writing: done=86 / total=205 (regressions=0).

The version pin is a decision, not a lag

Intercom publishes a description directory per API version, and 2.15 and 2.16 now exist upstream. This pack stays censused against 2.14, deliberately, on four converging facts:

  1. 2.14 is the version this pack's spec.source records and the census fixture is derived from (descriptions/2.14/api.intercom.io.yaml).
  2. 2.14 is what the first-party SDK defaults to — [email protected]'s BaseClient sends Intercom-Version: 2.14 when the caller pins nothing, so an unmodified SDK meets 2.14 shapes.
  3. 2.14 is the version every response shape in this twin was authored against, field by field, from that document's own examples.
  4. Nothing is refused for being newer: the twin accepts 2.15 and 2.16 on the header (and every version 1.0-2.16 plus Unstable), it simply renders 2.14 shapes underneath. That gap is named and filed — intercom.protocol.versioned_shapes (todo) — not hidden.

Moving the pin is a deliberate pack revision (re-fetch, re-derive the fixture, re-census, re-author whatever shapes changed), not a bump.

Prioritisation came from a real consumer rather than taste: dub's Intercom integration drives admins.identify, admins.list, contacts.search, contacts.find, contacts.create, conversations.create, conversations.reply and messages.create through [email protected]. Those are the operations modeled first and verified hardest.

Modeled (proven done, each with a failable offline verify):

  • Admins — GET /me (owner + workspace app), GET /admins, GET /admins/{id}, PUT /admins/{id}/away (away mode folds and comes back off).
  • Contacts — create / show / update / delete / list, POST /contacts/search, find_by_external_id, archive + unarchive (archiving genuinely removes the contact from the list and from search while a direct GET still resolves it), merge (a lead into a user). Vendor-faithful details: create answers 200, not 201; a duplicate email+role is a 409 naming the existing id while the same email as a lead is a different contact; PUT merges custom_attributes rather than replacing them.
  • Search — the published operator set (=, !=, IN, NIN, <, <=, >, >=, ~, !~, ^, $) really filters, nested AND/OR groups compose, and the body's pagination { per_page, starting_after } pages the result set.
  • Conversations — create (answering with the message shape carrying conversation_id, as the vendor does), retrieve / list / update / delete, search, reply as admin and as contact, parts (close / open / snooze / assign, including the assignee_id: "0" unassign sentinel), customers attach/detach, and conversation tags. open and snooze are accepted in the shape the spec documents them — without a type field — a type: "team" assignment is an honest 404 team_not_found rather than a false "admin-only" claim, and an undocumented message_type (including away_mode) is refused rather than fake-succeeded. Stateful: a contact reply reopens a closed conversation and increments count_reopens; removing the last customer is the vendor's 422. (A redundant close/open is a 400 here, which is a state-machine reading the vendor does not document either way — filed as intercom.conversations.redundant_transition_semantics, not asserted as vendor behaviour.)
  • Messages — POST /messages in-app and email, optionally opening a conversation (create_conversation_without_contact_reply), updating the recipient's last_contacted_at. An email without a subject is the vendor's 422.
  • Data events — POST /events (202 with an empty body, like the vendor), GET /events timeline and summary=true roll-up, and POST /events/summaries (200, not 202 — the spec declares different success codes for the two event endpoints, and the verify asserts both). Submitted summaries fold into the summary read and upsert by event_name, so a later batch never wipes an earlier one. Both the spec's single-object event_summaries and the array form callers actually send are accepted (the spec declares an object for a plural bulk field — recorded as intercom.events.summaries_payload_shape, not silently picked). A contact resolves by intercom id, user_id or email.
  • Data attributes — built-in (non-custom) attributes plus custom-attribute create/update, custom_attributes.<name> naming, the documented data_type: "options" list branch (stored and returned as string + options, as the vendor's own example shows), and archive/include_archived. The model enums differ by surface and the twin follows each: the GET filter accepts conversation, the create body does not. A built-in is not updatable (422 data_invalid); an unknown id is 404 field_not_found.
  • Tags / notes / segments / subscription types / companies — tag create-or-update, apply and remove on contacts and conversations, a refusal to delete a tag that is still applied (tag_has_dependent_objects); contact notes; segment read surface; subscription opt-in/out; company create-or-update keyed by the caller's own company_id with contact attach/detach.
  • Protocol — the error.list envelope with a field on validation errors, unmodeled ops 404 like the vendor, Intercom-Version accepted for every version Intercom publishes (1.0-2.16 plus Unstable) and 400 for a non-existent one — though the twin serves 2.14 shapes regardless (intercom.protocol.versioned_shapes, todo) — readOnly 405s writes, cursor pagination that walks the set exactly once, unix timestamps, and id minting that survives dirty state: delete→recreate, create-after-pull, pull-after-local-create, and a pulled admin or data attribute landing on exactly a seeded built-in's id — the real row keeps the contested id and the twin's stand-in is re-homed rather than deleted, so correctness does not rest on the synthetic id band being a lucky guess. (The per_page bounds and the unknown-cursor 400 are the twin's own reading, filed as intercom.protocol.pagination_limits_unconfirmed.)
  • Connector — pull contacts/conversations/tags/admins/data-attributes (idempotent, and the cursor walk is bounded), syncIntercomFromReal, and push that confirms only when the vendor did not answer with its error.list envelope. Push sends contact fields through an allowlist: twin bookkeeping (tag_ids, tag_applied_at, segment_ids, event_summaries, archived) can never reach a real workspace, and a write that touched only bookkeeping issues no request at all — an empty PUT would be a no-op the vendor still answers 200 to, which the connector would then wrongly confirm.
  • UI mirror — inbox list, contacts list, conversation detail, state pills, timestamps and nav, every one data-coupled to real twin state.

Fidelity. src/intercom-sdk.integration.test.ts drives the real, unmodified [email protected] over HTTP at the twin. The only thing configured is baseUrl, the SDK's own documented public option; the Intercom-Version stays the SDK's default 2.14, which is exactly the version this twin models. Two places the test has to widen a generated TYPE are SDK gaps, not twin gaps, and are called out in-line: CreateDataAttributeRequest omits name and model, and events.list() is typed as the summary item shape even though the non-summary response is data_event_list (recorded as intercom.events.list_response_shape).

Rate budget. Intercom documents 10,000 API calls/minute per app and 25,000/minute per workspace, enforced in 10-second sub-windows, with X-RateLimit-Limit/-Remaining/-Reset and a 429. The pack declares 600 weighted units / 60s — 6% of the per-app limit, deliberately far under it because the workspace limit is shared with every other installed app and because a 60s rolling window bounds the average rather than the burst. Message-delivering endpoints (POST /messages, POST /conversations, POST /conversations/{id}/reply) cost 5 because they reach a real human; POST /events and the two search endpoints cost 2. See src/intercom-budget.ts.

Twin-only control routes

POST /_twin/admins and POST /_twin/segments are twin-only test scaffolding, clearly namespaced outside Intercom's surface and deliberately absent from the capability manifest: admins and segments have no vendor create endpoint (they are workspace configuration), so a verify needs a way to build that state from nothing. Counting scaffolding as coverage would pad the denominator. A real client never calls them.

What this twin does not run

The Messenger is a hosted browser widget served from Intercom's own CDN with its own realtime transport, and this pack twins the REST API rather than that bundle. Outbound email / SMS / push delivery leaves the machine: the twin records the message and the conversation it opens, but puts mail in nobody's inbox. Fin's answers come from Intercom-hosted retrieval and model inference, and placing real calls needs carrier infrastructure — the call resources remain in scope as todos. Uploaded attachments live on Intercom-operated storage behind signed URLs; the twin models attachment metadata on a part. Intercom's proprietary reporting metric definitions (what counts as a first response, which SLA calendar applies, how CSAT is weighted) are unpublished; the computable part is filed as intercom.reporting.response_times, and conversation statistics over local state are already modeled.

Everything else not listed above is in scope and simply not built yet — see the todo entries in the manifest (tickets, articles + help center, news, visitors, custom objects, AI content, teams, calls, exports, webhooks and the OAuth token exchange).