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

@pryv/cmc

v3.9.0

Published

Cross-account Messaging & Consent client helpers for Pryv.io

Readme

CMC client helpers for pryv

Cross-account Messaging & Consent (CMC) client-side helpers for the Pryv JavaScript library. CMC enables federated consent + chat + system notifications across two Pryv accounts (same or different platforms) via the open-pryv.io cmc plugin.

Usage

@pryv/cmc is a sibling package to pryv — imported separately, not attached to the pryv namespace. It uses the standard pryv.Connection to talk to the server.

Importing

NPM

npm install --save pryv @pryv/cmc, then in your code:

const pryv = require('pryv');
const cmc = require('@pryv/cmc');

@pryv/cmc requires pryv@^3.3.0 as a peer dependency.

<script> tag

pryv-cmc.js must be loaded after pryv.js. The package exposes its API on the global pryvCmc:

<script src="https://api.pryv.com/lib-js/pryv.js"></script>
<script src="https://api.pryv.com/lib-js/pryv-cmc.js"></script>
<script>
  const cmc = window.pryvCmc;
</script>

Surface

@pryv/cmc ships two layers:

  • Level 0 — pure helpers (constants, slug + stream-id builders, classifiers, errorIds). Used to construct or parse identifiers without a network call.
  • Level 1 — protocol functions taking a pryv.Connection and a parameters object. Each maps to one server-side CMC operation.

Level 0 — constants + helpers

Namespace constants

  • cmc.NS = :_cmc: — reserved root for the CMC stream tree.
  • cmc.NS_INBOX = :_cmc:inbox — one-shot lifecycle stream (delivered events from peers).
  • cmc.NS_APPS = :_cmc:apps — per-app scope root.
  • cmc.NS_INTERNAL = :_cmc:_internal — plugin-private root.
  • cmc.NS_INTERNAL_RETRIES = :_cmc:_internal:retries — retry queue stream.

Event types

cmc.ET_REQUEST (consent/request-cmc), cmc.ET_ACCEPT (consent/accept-cmc), cmc.ET_REFUSE (consent/refuse-cmc), cmc.ET_REVOKE (consent/revoke-cmc), cmc.ET_INVALIDATE_LINK (consent/invalidate-link-cmc), cmc.ET_SCOPE_REQUEST (consent/scope-request-cmc), cmc.ET_SCOPE_UPDATE (consent/scope-update-cmc), cmc.ET_CHAT (message/chat-cmc), cmc.ET_SYSTEM_ALERT (notification/alert-cmc), cmc.ET_SYSTEM_ACK (notification/ack-cmc).

Grouped collections: cmc.EVENT_TYPES_LIFECYCLE, cmc.EVENT_TYPES_CHAT, cmc.EVENT_TYPES_SYSTEM.

Slug + stream-id builders

cmc.slugifyHost('pryv.me');                                     // → 'pryv-me'
cmc.counterpartySlug({ username: 'alice', host: 'pryv.me' });   // → 'alice--pryv-me'
cmc.parseCounterpartySlug('alice--pryv-me');                    // → { username: 'alice', hostSlug: 'pryv-me' }

cmc.appScope('my-app');                                         // → ':_cmc:apps:my-app'
cmc.chatStreamUnder(':_cmc:apps:my-app', 'alice--pryv-me');     // → ':_cmc:apps:my-app:chats:alice--pryv-me'
cmc.collectorStreamUnder(':_cmc:apps:my-app', 'alice--pryv-me');// → ':_cmc:apps:my-app:collectors:alice--pryv-me'

cmc.SEPARATOR is --.

Classifiers + parsers

cmc.isCmcStreamId(':_cmc:apps:my-app');                  // → true
cmc.isAppNestedPluginStream(':_cmc:apps:my-app:chats:s'); // → true
cmc.getAppCode(':_cmc:apps:my-app:study-1');              // → 'my-app'
cmc.parseChatStreamId(':_cmc:apps:my-app:chats:a--p-me'); // → { appCode, scopeStreamId, counterpartySlug, counterparty }
cmc.parseCollectorStreamId(...);                          // → { ... } same shape

Error id catalogue

cmc.errorIds mirrors the server-side CmcErrorIds catalogue (31 stable kebab-case strings as of @pryv/[email protected]). Match on these constants when observing trigger.content.failure.reason or error.data.id from a failed events.create / accesses.create instead of parsing English error.message.

Lifecycle / handler / chat-routing examples: cmc.errorIds.CAPABILITY_INVALID ('cmc-capability-invalid'), cmc.errorIds.CAPABILITY_CONSUMED, cmc.errorIds.CAPABILITY_INVALIDATED, cmc.errorIds.CAPABILITY_ALREADY_ACCEPTED_BY_YOU, cmc.errorIds.HANDLER_DELIVERY_FAILED, cmc.errorIds.CHAT_NO_REMOTE_APIENDPOINT, etc.

Added in @pryv/[email protected] (server-side open-pryv.io ≥ 2.0.0-pre.4):

| Const | String | When you'll see it | |---|---|---| | CAPABILITY_TTL_OUT_OF_RANGE | 'cmc-capability-ttl-out-of-range' | createInvite({ expiresAt }) resolves to a TTL outside [60s, 30d]. Omit expiresAt to use the 7-day default. | | HANDLER_MISSING_CAPABILITY_ID | 'cmc-handler-missing-capability-id' | Plugin handler couldn't find content.capabilityId on the trigger event. | | CHAT_DISABLED | 'cmc-chat-disabled' | sendChat against a relationship whose negotiated features.chat: false. Default-permit on omission. | | SYSTEM_MESSAGING_DISABLED | 'cmc-system-messaging-disabled' | sendSystemAlert / ack against features.systemMessaging: false. Scope-request / scope-update remain permitted regardless. | | CLIENTDATA_CMC_FORBIDDEN | 'cmc-clientdata-cmc-forbidden' | accesses.create / accesses.update rejected user-supplied clientData.cmc.* (the namespace is plugin-owned). | | RESERVED_STREAM_UNDELETABLE | 'cmc-reserved-stream-undeletable' | streams.delete rejected on a plugin-managed :_cmc:* parent (incl. personal-token deletes). | | COUNTERPARTY_IDENTITY_MISSING | 'cmc-counterparty-identity-missing' | Peer-side content.from stamping hook rejected — the counterparty access lacks {username,host} (ops-level, shouldn't happen under normal flow). |

The full list lives in src/index.js (and the typed mirror in src/index.d.ts).

cmc.CmcError is a typed Error subclass carrying .id (one of errorIds.*) and .failure (the server's raw failure object). It's thrown by acceptInvite on status: 'failed'.

Capability TTL bounds

The server bounds content.request.expiresAt (Unix seconds) to [60s, 30d] from now at mint time. Pass expiresAt to cmc.createInvite() to override the default 7-day lifetime; omit it for the default. Out-of-range values reject with cmc-capability-ttl-out-of-range before the capability access is minted.

Features negotiation

cmc.createInvite({ features: { chat, systemMessaging } }) opts the relationship in or out of each cross-account channel.

  • Both default to true when the key is omitted (matches the offer-side default).
  • Setting either to false is binding on both sides at send time — the recipient's plugin records the negotiation on the counterparty access's clientData.cmc.features.*, and subsequent sendChat / sendSystemAlert against the access reject with cmc-chat-disabled / cmc-system-messaging-disabled until a new invite negotiates it back on.
  • cmc.scopes.{inbox, chats, collectors} and cmc.revokeRelationship are unaffected — those are protocol surfaces, not message channels.

pryv.utils.decomposeAPIEndpoint

The companion pryv.utils.decomposeAPIEndpoint(apiEndpoint, serviceInfoApi) returns { token, username, host } with host being the canonical platform host (no <username>. subdomain prefix). It's the right way to derive the { username, host } arguments for cmc.counterpartySlug. It lives in pryv.utils (not cmc.*) because it's generally useful — see the pryv README.

Level 1 — protocol functions

Each function takes a pryv.Connection and a parameters object. All return Promises. The full TypeScript signatures live in src/index.d.ts.

Provider side (the requester)

const invite = await cmc.createInvite(conn, {
  appCode: 'my-app',
  scopeStreamId: ':_cmc:apps:my-app:study-1',
  displayName: 'Dr. Alice',
  requestedPermissions: [{ streamId: 'fertility', level: 'read' }],
  mode: 'single-use',            // or 'open-link'
  consent: { en: 'I consent to share data for this study.' },
  features: { chat: true, systemMessaging: true }
});
// invite = { inviteEventId, capabilityUrl, mode, expiresAt }

const arrived = await cmc.waitForAccept(conn, {
  fromUsername: 'bob',           // identify the expected accepter
  appCode: 'my-app',
  timeoutMs: 15000
});
// arrived = { acceptInboxEventId, grantedAccessApiEndpoint, counterparty, features }
// grantedAccessApiEndpoint is the data-grant access on bob's account —
// use it to query bob's data.

await cmc.revokeRelationship(conn, {
  inviteEventId: invite.inviteEventId,     // OR pass { accessId, scopeStreamId } directly
  reason: { en: 'study completed' }
});

// Open-link mode only (Phase 2 lifecycle):
await cmc.invalidateCapability(conn, {
  inviteEventId: invite.inviteEventId,
  reason: { en: 'study cohort closed' }
});

const list = await cmc.listInvites(conn, { appCode: 'my-app', limit: 1000 });
// list = { items: InviteRecord[], truncated: boolean }

const one = await cmc.getInviteStatus(conn, invite.inviteEventId);

const update = await cmc.proposeScopeUpdate(conn, {
  collectorStreamId: ':_cmc:apps:my-app:collectors:bob--pryv-me',
  newPermissions: [{ streamId: 'fertility', level: 'read' }, { streamId: 'cycle', level: 'read' }],
  message: { en: 'Adding cycle stream for sub-study.' }
});
// update = { scopeRequestEventId }
// (renamed in 3.9.0 from `requestScopeUpdate` — that name now refers to
//  the user-side accept hand-off helper. Old name removed; importers
//  must use `proposeScopeUpdate`.)

Consumer side (the accepter)

Token class. consent/accept-cmc and consent/scope-update-cmc writes require a personal access token server-side (Pryv.io rejects app/shared tokens with 400 invalid-operation + error.data.id === 'cmc-accept-requires-personal-token'). If your app holds only an app/shared token, use the accept hand-off helpers instead of calling acceptInvite / acceptScopeUpdate directly.

Revoke uses the standard access-permission gate, not the personal-token gate. consent/revoke-cmc is checked against AccessLogic.canDeleteAccess (which honours the selfRevoke feature permission on the target access). Apps holding a relationship's data-grant access can self-revoke directly via cmc.revokeAcceptance(...) / cmc.revokeRelationship(...) — no hand-off needed. The server rejects unauthorised revokes with error.data.id === 'cmc-revoke-forbidden'.

const offer = await cmc.readOffer(capabilityUrl);
// offer = { requester: { username, host, displayName }, consent, requestedPermissions, mode, features }

const accepted = await cmc.acceptInvite(conn, capabilityUrl, {
  scopeStreamId: ':_cmc:apps:my-app',      // REQUIRED — own scope stream where the trigger lands.
  extra: { chat: true, systemMessaging: true },
  accessName: 'my-app-grant'
});
// accepted = { acceptEventId, dataGrantAccessId, counterparty, features }
// Waits for Phase-2 completion by default (~50-200ms).
// Pass `waitForCompletion: false` to return immediately as { ..., status: 'pending' }.
// Requires `conn` to be authenticated with a personal token (see hand-off helpers below).

await cmc.refuseInvite(conn, capabilityUrl, {
  scopeStreamId: ':_cmc:apps:my-app',
  reason: { en: 'not interested' }
});

await cmc.revokeAcceptance(conn, {
  scopeStreamId: ':_cmc:apps:my-app',
  accessId: accepted.dataGrantAccessId,
  reason: { en: 'opting out' }
});

const relationships = await cmc.listAcceptedRelationships(conn, { appCode: 'my-app' });
// relationships = RelationshipRecord[]

// Respond to a scope-update request from the provider:
await cmc.acceptScopeUpdate(conn, scopeRequestEventId);
await cmc.refuseScopeUpdate(conn, scopeRequestEventId, { reason: { en: 'no thanks' } });

Accept hand-off (app without a personal token)

acceptInvite and acceptScopeUpdate post triggers that the server gates to personal tokens only. If your app holds only an app/shared access, hand the user off to app-web-auth3's /cmc-accept or /cmc-scope-update page: the user authenticates with their own credentials, the page writes the trigger with the fresh personal token, and the result is returned to your app via popup postMessage or returnUrl redirect.

Revoke does NOT need a hand-off — cmc.revokeAcceptance / cmc.revokeRelationship go through the standard AccessLogic.canDeleteAccess rule (which honours the selfRevoke feature permission on the target). Apps holding the relationship's data-grant access can self-revoke from any token class.

Two helper pairs, both ship in @pryv/[email protected]:

// 1. URL only (caller drives navigation — custom popup, mobile deep-link, …).
const url = cmc.requestAcceptUrl({
  authUrl: 'https://access.pryv.me/access/v3/cmc-accept', // /cmc-accept route on the deployed app-web-auth3
  pryvApi: 'https://reg.pryv.me/',                        // recipient's Pryv API base
  capabilityUrl,                                           // from the requester's invite (out-of-band)
  scopeStreamId: ':_cmc:apps:my-app',                     // recipient's own :_cmc:apps:* stream
  accessName: 'my-app-grant'                              // optional
  // returnUrl: 'https://app.example.com/accepted'         // switches to redirect mode
});

// 2. Popup + postMessage (browser-only).
const result = await cmc.requestAccept({
  authUrl,
  pryvApi,
  capabilityUrl,
  scopeStreamId: ':_cmc:apps:my-app',
  // mode: 'popup' is the default. timeoutMs default: 10 min.
});
// result = { ok: true, dataGrantApiEndpoint, acceptEventId }
// Rejects with CmcError (id: 'cmc-accept-popup-closed' | 'cmc-accept-popup-blocked'
// | 'cmc-accept-timeout' | the server's `failure.reason` on ok:false).

// Or redirect mode (full-page navigation; the page returns by re-navigating to returnUrl):
await cmc.requestAccept({
  authUrl, pryvApi, capabilityUrl, scopeStreamId: ':_cmc:apps:my-app',
  returnUrl: 'https://app.example.com/accepted'   // → location.assign(returnUrl?cmcAcceptResult=<json>)
});

Scope-update hand-off (same shape, different page):

// Browser flow — open the auth page and wait for the user to accept / refuse.
const result = await cmc.requestScopeUpdate({
  authUrl: 'https://access.pryv.me/access/v3/cmc-scope-update', // /cmc-scope-update route
  pryvApi: 'https://reg.pryv.me/',
  scopeRequestEventId: 'evt-scope-req-abc123',                  // from the collector's proposal
  // scopeStreamId is optional — defaults to the scope-request event's home stream
});
// result = { ok: true, updateEventId, action: 'accept' | 'refuse' }
// Rejects with CmcError (id: 'cmc-scope-update-popup-closed' | 'cmc-scope-update-popup-blocked'
// | 'cmc-scope-update-timeout' | the server's `failure.reason` on ok:false).

// URL-only (caller drives navigation):
const url = cmc.requestScopeUpdateUrl({
  authUrl, pryvApi, scopeRequestEventId, returnUrl: 'https://app.example.com/done'
});

The /cmc-accept page is part of app-web-auth3; operators serving Pryv.io's auth pages get it automatically. The result payload (popup or redirect) carries { type: 'cmc-accept-result', ok, dataGrantApiEndpoint, acceptEventId }.

Cross-direction (chat, system messages)

await cmc.sendChat(conn, {
  scopeStreamId: ':_cmc:apps:my-app',
  peerSlug: cmc.counterpartySlug({ username: 'bob', host: 'pryv.me' }),
  content: 'Hello Bob.'
});
// Schema: { content: string } (1-10240 chars per data-types/message.json#chat-cmc).

await cmc.sendSystemAlert(conn, {
  scopeStreamId: ':_cmc:apps:my-app',
  peerSlug,
  level: 'warning',
  title: { en: 'Battery low' },
  body: { en: 'Charge device before next sync.' },
  ackRequired: true,
  ackId: 'BAT-001'
});

await cmc.sendSystemAck(conn, {
  scopeStreamId: ':_cmc:apps:my-app',
  peerSlug,
  alertEventId,
  ackId: 'BAT-001'
});

Observation scopes

Use these helpers to build the Monitor scope for the add-on @pryv/monitor:

const inboxScope = cmc.scopes.inbox();
// → { streams: [':_cmc:inbox'] }
const chatsScope = cmc.scopes.chats({ appCode: 'my-app' });
// → { streams: [':_cmc:apps:my-app:chats'] } (recursive)
const peerScope = cmc.scopes.chats({ appCode: 'my-app', peerSlug: 'bob--pryv-me' });
// → { streams: [':_cmc:apps:my-app:chats:bob--pryv-me'] } (narrowed)
const collectorsScope = cmc.scopes.collectors({ appCode: 'my-app', peerSlug });

Plug them into a pryv.Monitor to receive live updates:

const monitor = new pryv.Monitor(conn, cmc.scopes.inbox())
  .on('event', (event) => {
    if (event.type === cmc.ET_ACCEPT) { /* a peer accepted */ }
    if (event.type === cmc.ET_REVOKE) { /* a peer revoked */ }
  });
await monitor.start();

Examples

Full handshake (provider → consumer)

const pryv = require('pryv');
const cmc = require('@pryv/cmc');

const provider = new pryv.Connection('https://[email protected]/');
const consumer = new pryv.Connection('https://[email protected]/');

// Alice issues an invite.
const invite = await cmc.createInvite(provider, {
  appCode: 'my-app',
  scopeStreamId: ':_cmc:apps:my-app:study-1',
  displayName: 'Dr. Alice',
  requestedPermissions: [{ streamId: 'fertility', level: 'read' }],
  consent: { en: 'I consent to share fertility data for this study.' },
  features: { chat: true }
});

// (Share invite.capabilityUrl with Bob out-of-band — QR / link / email.)

// Bob accepts.
const accepted = await cmc.acceptInvite(consumer, invite.capabilityUrl, {
  scopeStreamId: ':_cmc:apps:my-app',
  extra: { chat: true }
});

// Alice waits for Bob's accept arrival + reads Bob's data.
const arrived = await cmc.waitForAccept(provider, {
  fromUsername: accepted.counterparty.username,
  appCode: 'my-app'
});
const peerConn = new pryv.Connection(arrived.grantedAccessApiEndpoint);
const data = await peerConn.api([{ method: 'events.get', params: { streams: ['fertility'], limit: 100 } }]);

Known limitation — waitForAccept identification

The accept arrival on :_cmc:inbox does not carry the requester's inviteEventId (the server-side originalEventId field is the capability-internal offer event id, not the trigger). cmc.waitForAccept therefore matches by from.{username, host} + requesterAppCode instead. If you don't know the accepter's username ahead of time (e.g. open-link mode where anyone with the URL may accept), poll the inbox yourself using events.get with streams: [cmc.NS_INBOX] and filter on what you do know.

Contributing

See the Pryv JavaScript library README

License

BSD-3-Clause