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

@memberjunction/lists

v5.41.0

Published

MemberJunction: Lists — server-side TypeScript core for list materialization, set algebra, delta computation, lineage tracking, refresh modes, and sharing/invitations. Writes to MJ entities (List Detail, Resource Permission, List Invitation, Audit Log) —

Downloads

2,759

Readme

@memberjunction/lists

Pure TypeScript core for MemberJunction Lists. Owns list materialization, set algebra, delta computation, lineage tracking, refresh modes, drop guards, sharing, and audience resolution. Framework-agnostic — no GraphQL, HTTP, or Angular dependencies.

This is the source of truth for the type contracts that the rest of the stack (the ListOperationsResolver in @memberjunction/server, the GraphQLListsClient in @memberjunction/graphql-dataprovider, the Phase-N list Actions in @memberjunction/core-actions, and the Angular components in @memberjunction/ng-list-management) all import from.

Installation

npm install @memberjunction/lists

What's in here

ListOperations    → materialize / refresh / compose / preview-and-apply
ListSharing       → direct shares, email invitations, audit log, capabilities
AudienceResolver  → friendly-named re-export of ResolveSource (Communications)
deltaToken        → HMAC-signed preview-token contract (server enforced)
types             → ListSource, ListDelta, ListCapabilities, etc.

Public API surface

Core types (types.ts)

type ListRefreshMode = 'Additive' | 'Sync';

type ListSource =
  | { kind: 'list'; listId: string }
  | { kind: 'view'; viewId: string; runtimeParams?: Record<string, unknown> }
  | { kind: 'adhoc'; entityName: string; extraFilter: string };

interface ResolvedRecordSet {
  EntityName: string;
  RecordIds: string[];       // MJ: List Detail.RecordID format
  TotalCount?: number;
}

interface ListDelta {
  TargetListId: string | null;
  EntityName: string;
  ToAdd: string[];
  ToRemove: string[];
  Unchanged: string[];
  Counts: { Add; Remove; Unchanged; SourceTotal; TargetTotal };
  Warnings: ListDeltaWarning[];
  DeltaToken: string;        // HMAC, 5-min TTL
}

interface ApplyResult {
  Success: boolean;
  ResultCode: 'SUCCESS' | 'DROP_NOT_CONFIRMED' | 'STALE_DELTA' |
              'INVALID_TOKEN' | 'PERMISSION_DENIED' | 'TARGET_NOT_FOUND' |
              'PARTIAL_SUCCESS' | 'UNEXPECTED_ERROR';
  Message: string;
  CreatedListId?: string;
  TargetListId?: string;
  Counts?: { Added; Removed; Failed };
  Errors?: string[];
}

ListOperations

Constructor: new ListOperations(contextUser, provider?).

// Read-only
ResolveSource(source: ListSource): Promise<ResolvedRecordSet>;
GetListMembers(listId: string): Promise<ResolvedRecordSet>;

// Preview (never mutates) — returns a signed DeltaToken
ComputeDelta(target: ListSource | 'new', source: ListSource, mode: ListRefreshMode): Promise<ListDelta>;
ComputeSetOp(op: 'union' | 'intersection' | 'difference', inputs: ListSource[], target?: ListSource | 'new'): Promise<ListDelta>;

// Commit (server enforces token validity + drop guard)
ApplyDelta(delta: ListDelta, opts: { ConfirmDrops: boolean; DeltaToken: string }): Promise<ApplyResult>;

// High-level convenience
MaterializeFromView(viewId: string, opts: MaterializeOptions): Promise<ApplyResult>;
AddViewResultsToList(viewId: string, listId: string): Promise<ApplyResult>;
RefreshFromSource(listId: string, mode: ListRefreshMode, opts: { ConfirmDrops: boolean }): Promise<ApplyResult>;

The drop-row warning contract

Every mutating list operation goes through ComputeDelta first. The returned DeltaToken is an HMAC over {targetListId, source signature, mode, timestamp} with a 5-minute TTL.

ApplyDelta enforces, server-side, in this order:

  1. Token signature + 5-min TTL — else INVALID_TOKEN.
  2. Re-compute against current target state — else STALE_DELTA.
  3. Counts.Remove > 0 && !ConfirmDrops — else DROP_NOT_CONFIRMED.
  4. Editor permission on target (placeholder; Phase 2 wires the real ResourcePermission check).

The Angular <mj-list-delta-confirm> dialog is the only UI path that produces a valid token. The server rejects drops regardless of UI.

ListSharing

Constructor: new ListSharing(contextUser, provider?).

type SharePermissionLevel = 'View' | 'Edit' | 'Owner';

type ShareTarget =
  | { kind: 'user'; userId: string }
  | { kind: 'role'; roleId: string };

// Direct shares (recipient already has an MJ account)
Share(args: { ListID; Target; PermissionLevel }): Promise<ShareResult>;
Unshare(permissionId: string): Promise<ShareResult>;
GetSharesForList(listId: string): Promise<ListShareSummary[]>;
GetListsSharedWithUser(): Promise<SharedListSummary[]>;

// Email invitations (recipient may not yet have an account)
Invite(args: { ListID; Email; Role: 'Editor' | 'Viewer'; TtlMs? }): Promise<InviteResult>;
AcceptInvitation(token: string): Promise<AcceptInvitationResult>;
RevokeInvitation(invitationId: string): Promise<ShareResult>;

// Permission introspection for UI gating
ResolveEffectivePermission(listId: string): Promise<SharePermissionLevel | null>;

Every mutation also writes an MJ: Audit Logs entry. Audit log types are seeded via metadata sync (see metadata/audit-log-types/.list-sharing-audit-types.json).

Direct user-shares fan out via the platform CreateShareNotification dispatcher (registered by @memberjunction/notifications) — recipients get in-app + email notifications per their preferences. Role-target shares are not fanned out per-user (that's a separate worker).

Capability gating

UI components should derive button visibility from the caller's effective permission level, not from raw entity-permission flags. The mapping is centralized so UI and server agree:

const level: SharePermissionLevel | null =
  await sharing.ResolveEffectivePermission(listId);

const caps: ListCapabilities = capabilitiesForLevel(level);
// caps.CanEdit / CanRefresh / CanShare / CanDelete / CanRunOperations
// Server-side enforcement remains the source of truth — these are a
// UX convenience so users don't see buttons they'll be rejected on.

Mapping per mockup 19:

| Level | Read | Edit | Refresh | Share | Delete | Operations | | ------- | :--: | :--: | :-----: | :---: | :----: | :--------: | | Owner | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Editor | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | | Viewer | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | | null | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |

AudienceResolver

Friendly-named entry point for resolving an AudienceSource (= ListSource) to a ResolvedRecordSet. Identical semantics to ListOperations.ResolveSource, exposed under a name that reads naturally for Communications and other audience-aware code.

const resolver = new AudienceResolver(contextUser, provider);
const result = await resolver.Resolve({ kind: 'list', listId });
// result.RecordIds — array of record IDs in the target audience

deltaToken

Internal helpers exposed for resolver-layer use. Server bootstrap must set the HMAC secret:

import { SetDeltaTokenSecret } from '@memberjunction/lists';

SetDeltaTokenSecret(process.env.MJ_LIST_DELTA_SECRET);

Falls back to the env var MJ_LIST_DELTA_SECRET if no explicit call is made. Refuses to use a default secret — silent fallback would let a misconfigured server hand out forgeable tokens.

Examples

Save a view's current results as a new list with lineage

const ops = new ListOperations(currentUser, provider);
const result = await ops.MaterializeFromView('view-abc', {
  ListName: 'Q4 VIP Donors',
  Description: 'Materialized from the Q4 Donor View',
  CategoryId: 'cat-marketing',
  RememberLineage: true,     // can refresh from source later
  UseSnapshot: false,        // refresh re-reads the live view
  RefreshMode: 'Additive',   // default refresh mode for this list
});
// result.CreatedListId, result.Counts.Added

Refresh a lineage-bearing list (Sync mode, will drop missing rows)

const result = await ops.RefreshFromSource(listId, 'Sync', {
  ConfirmDrops: true,    // required when Sync mode would remove records
});

Preview a 3-way intersection without committing

const delta = await ops.ComputeSetOp('intersection', [
  { kind: 'list', listId: 'list-a' },
  { kind: 'view', viewId: 'view-b' },
  { kind: 'view', viewId: 'view-c' },
]);
// delta.ToAdd has the resulting record IDs; nothing has been saved.

Share a list with an Editor

const sharing = new ListSharing(currentUser, provider);
const result = await sharing.Share({
  ListID: 'list-abc',
  Target: { kind: 'user', userId: 'user-xyz' },
  PermissionLevel: 'Edit',
});
// In-app + email notification fires automatically via the platform handler.

Invite someone by email

const inv = await sharing.Invite({
  ListID: 'list-abc',
  Email: '[email protected]',
  Role: 'Editor',
  TtlMs: 7 * 24 * 60 * 60 * 1000,
});
// inv.Token + inv.ExpiresAt — the caller builds the accept URL.

Architecture

Sits at the bottom of a four-layer stack:

┌────────────────────────────────────────────────────────────┐
│  Angular UI (@memberjunction/ng-list-management,           │
│  @memberjunction/ng-explorer-core, etc.)                   │
└────────────────────────────┬───────────────────────────────┘
                             ↓ uses
┌────────────────────────────────────────────────────────────┐
│  GraphQLListsClient (@memberjunction/graphql-dataprovider) │
└────────────────────────────┬───────────────────────────────┘
                             ↓ wire format
┌────────────────────────────────────────────────────────────┐
│  ListOperationsResolver (@memberjunction/server)           │
└────────────────────────────┬───────────────────────────────┘
                             ↓ delegates to
┌────────────────────────────────────────────────────────────┐
│  ListOperations / ListSharing / AudienceResolver  ← this   │
│  (@memberjunction/lists)                                   │
└────────────────────────────────────────────────────────────┘

Every layer is a thin pass-through. The Actions in @memberjunction/core-actions (MaterializeListFromViewAction, ShareListAction, etc.) are parallel consumers — they import directly from this package so AI agents, workflows, and scheduled jobs get the same capability as the UI.

Dependencies

  • @memberjunction/coreMetadata, RunView, UserInfo, IMetadataProvider
  • @memberjunction/core-entitiesMJListEntity, MJResourcePermissionEntity, etc.
  • @memberjunction/global — class registration utilities

No HTTP, no GraphQL, no Angular.

Testing

cd packages/Lists
npm run test

76 vitest cases across deltaToken (HMAC roundtrip + secret resolution), ListOperations (math, set-ops, drop guard, materialize, refresh modes, snapshot vs live), AudienceResolver, and ListSharing (share / invite / accept / revoke lifecycle, capability mapping).

Coverage tracked at 88%+ lines; reports via npm run test:coverage.