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

@ceros-dev/markup-sdk

v1.0.0-rc.1

Published

Browser SDK for embedding MarkUp commenting and collaboration features

Readme

@ceros-dev/markup-sdk

Browser SDK for embedding MarkUp commenting and collaboration features in web applications.

Browser-only. The SDK touches the DOM (document) at module load, so it must run in a browser — it is not usable in a Node/SSR runtime. In frameworks like Next.js, import it only in client components (e.g. behind a "use client" boundary or a dynamic import()), never during server rendering.

Stability. Follows semantic versioning from 1.0.0 — no breaking changes to the public API within a major version. Published to public npm and the sdk.markup.io CDN.

For raw HTTP-client usage without managed auth or service helpers see @ceros-dev/markup-sdk-core, the headless transport layer this package builds on top of.

Installation

npm

npm install @ceros-dev/markup-sdk

CDN (script tag)

Every release is also published to the MarkUp CDN as self-contained browser bundles — no npm install or registry auth needed:

<!-- Full UI: toolbar, pins, thread popover, commenting mode -->
<script src="https://sdk.markup.io/v1.0.0/markup-sdk-ui.min.js"></script>
<script>
    const markup = MarkUpSDK.init({
        publicKey: "your-public-key",
        markupId: "00000000-0000-0000-0000-000000000000"
    });
    markup.render();
</script>

The bundle installs a single MarkUpSDK global — the same class this package exports — so everything in the Quickstart below applies unchanged. The other runtime exports (MarkUpSDKError, ConfigurationError, SDKStateError, SDKErrorCode, configureLogger, and notify in the UI bundle) are attached to the global as properties.

Each prefix on the CDN serves five files: markup-sdk-ui.min.js (full UI, classic script), markup-sdk.min.js (API-only, classic script), markup-sdk-ui.esm.js / markup-sdk.esm.js (the same two as ES modules), and manifest.json (exact version, commit, and a sha384 Subresource Integrity hash per file).

| URL prefix | Points at | Caching | | --- | --- | --- | | /v<version>/ (e.g. /v1.0.0/) | that exact release | immutable, 1 year | | /latest/ | newest stable release | 5 minutes | | /v<major>/ (e.g. /v1/) | newest stable release within that major | 5 minutes | | /next/ | newest prerelease | 5 minutes |

/v1/ and /latest/ track the newest stable release and are convenient for development; /next/ serves the newest prerelease.

For production, pin an exact version and add the SRI hash from that version's manifest.json:

<script
    src="https://sdk.markup.io/v1.0.0/markup-sdk-ui.min.js"
    integrity="sha384-…from manifest.json…"
    crossorigin="anonymous"
></script>

The ESM bundles work the same way from <script type="module">:

<script type="module">
    import {MarkUpSDK} from "https://sdk.markup.io/v1.0.0/markup-sdk-ui.esm.js";

    MarkUpSDK.init({publicKey: "your-public-key", markupId: "…"}).render();
</script>

Entry Points

The package exposes two subpaths so consumers can opt into the bundle weight they need:

  • @ceros-dev/markup-sdk/ui — full UI: toolbar, pins, thread popover, commenting mode. Bundles Preact and prebuilt components. The Quickstart below targets this entry.
  • @ceros-dev/markup-sdk — API only: services, auth, events. No rendered UI (no render(), toolbar, or components). Use this when you render your own UI and only need typed access to the MarkUp API. markup.render() is unavailable on this entry; everything else (init, on/off, threads, comments, projects, uploads, destroy) is identical.

Quickstart

import {MarkUpSDK} from "@ceros-dev/markup-sdk/ui";

const markup = MarkUpSDK.init({
    publicKey: "your-public-key",
    markupId: "00000000-0000-0000-0000-000000000000",
    onTokenNeeded: async () => {
        const res = await fetch("/api/markup-token");
        const {token} = await res.json();
        return token;
    }
});

markup.render();

Lifecycle

MarkUp is a singleton. The flow is init → render → destroy, and destroy() must be called before re-initializing (e.g. on SPA unmount or hot-reload).

const markup = MarkUpSDK.init({publicKey, markupId, onTokenNeeded});
markup.render({position: "bottom-right", keyboardShortcuts: true});

// Subscribe to events
const off = markup.on("thread:open", ({threadId}) => {
    console.log("opened", threadId);
});

// Later, on teardown
off();
markup.destroy();
  • init(options) — validates options, constructs the singleton. Calling init twice without destroy in between returns the existing instance and logs a warning.
  • render(options?) — mounts the shadow-DOM container, toolbar, pins, and panels, then runs the authentication gate to resolve a session and load live data. Calling render twice is a no-op and logs a warning.
  • destroy() — unmounts all UI, cancels in-flight work (including any pending silent sign-in attempt), clears auth + stores, removes the SPA navigation listener, and releases the singleton slot so init can be called again.

Authentication

MarkUp supports two authentication modes; you choose by whether you pass onTokenNeeded. The SDK resolves a session in this order: onTokenNeeded → cached session → silent SSO → interactive sign-in.

1. Bring your own token (onTokenNeeded)

Provide onTokenNeeded to authenticate users with your own backend. The SDK calls it when it needs a token and exchanges the signed JWT for a MarkUp session (as in the Quickstart above):

const markup = MarkUpSDK.init({
    publicKey: "your-public-key",
    markupId: "00000000-0000-0000-0000-000000000000",
    onTokenNeeded: async () => {
        const res = await fetch("/api/markup-token");
        const {token} = await res.json();
        return token;
    }
});

2. SDK-managed "Sign in via MarkUp"

Omit onTokenNeeded and the SDK handles sign-in itself:

  1. Silent SSO — on render() the SDK makes a single silent attempt through a hidden, origin-locked iframe. If the user already has a MarkUp session they're signed in with no interaction.
  2. Interactive sign-in — if the silent attempt fails, the toolbar shows a Sign in button. Clicking it opens a MarkUp sign-in popup; on success the SDK loads data automatically.

Both flows work for any registered MarkUp user — the user does not need to be a member of the MarkUp's workspace. If the MarkUp has public share-link access enabled ("Anyone with the share link has access"), a signed-in non-member is granted comment access, mirroring guest access on the MarkUp website. If the MarkUp is private and the user isn't a member, sign-in rejects with WORKSPACE_FORBIDDEN.

Resolved sessions are cached (per the sessionStorage option) and refreshed automatically before they expire. Interactive sign-in can reject with a MarkUpSDKError whose code is one of:

| SDKErrorCode | Meaning | | --------------------- | ------------------------------------------------------------- | | POPUP_BLOCKED | The browser blocked the sign-in popup (window.open failed). | | POPUP_CLOSED | The user closed the popup before completing sign-in. | | WORKSPACE_FORBIDDEN | The user can't access this MarkUp — not a workspace member and the MarkUp has no active public share link. | | SDK_SIGNIN_FAILED | Sign-in failed for another reason. |

Origins are build-time. The MarkUp API origin and the sign-in website origin are frozen into the bundle at build time (from MARKUP_API_URL / MARKUP_APP_URL) — there is no runtime URL option in init. The website origin is also the only origin the SDK accepts sign-in messages from.

Configuration Options (MarkUpSDK.init)

| Option | Type | Required | Default | Notes | | ---------------- | ------------------------------------------------ | -------- | ----------------- | ------------------------------------------------------------------ | | publicKey | string | yes | — | SDK installation public key | | markupId | string (UUID) | yes | — | MarkUp resource ID; must be a valid UUID | | onTokenNeeded | () => Promise<string> | no | — | Returns a signed JWT. Omit to use the built-in Sign in via MarkUp flow | | sessionStorage | "memory" \| "sessionStorage" \| "localStorage" | no | "memory" | Where to persist the resolved session | | debug | boolean | no | false | Enables debug-level console logging |

Render Options (markup.render)

| Option | Type | Default | Notes | | -------------------- | --------------------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------ | | position | "bottom-center" \| "bottom-right" \| "bottom-left" \| "top-center" \| "top-right" \| "top-left" | "bottom-center" | Toolbar anchor | | offset | {x: number; y: number} | {x: 16, y: 16} | Pixel offset from the chosen anchor | | theme | "light" \| "dark" \| "auto" \| "inverted" | "auto" | Color theme for SDK UI | | collapsed | boolean | false | Start the toolbar collapsed | | draggable | boolean | true | Allow the user to drag the toolbar | | persistPosition | boolean | false | Persist the dragged position across reloads | | container | HTMLElement \| string | document.body | Mount point for the shadow-DOM host; string is treated as a CSS selector | | zIndex | number | very large | z-index for the shadow container (tune if the SDK covers host overlays) | | keyboardShortcuts | boolean | true | Global shortcuts (e.g. press C to toggle commenting mode); set to false to disable | | commentableContainer | HTMLElement \| string \| Array<HTMLElement \| string> | (unrestricted) | Restrict comment placement to one or more containers (CSS selector and/or element). See Restricting where comments can be placed |

Toggle shortcuts at runtime after render() with markup.setKeyboardShortcutsEnabled(enabled).

Restricting where comments can be placed

By default a comment pin can be placed on any element on the page. Pass commentableContainer to render() to confine commenting to one or more containers — useful when only a specific region (e.g. a document body or a preview frame) should be annotatable.

// Single container by selector
markup.render({commentableContainer: "#main-content"});

// An element reference works too
const doc = document.querySelector<HTMLElement>(".doc");
if (doc) {
    markup.render({commentableContainer: doc});
}

// Multiple containers — selectors and elements can be mixed
markup.render({commentableContainer: ["#doc", "#sidebar", someElement]});

A pin may be placed on — or dragged onto — an element only if it is, or is inside, one of the configured containers. Clicking a placeable element outside the allowed area swallows the click and surfaces a snackbar hint instead of placing a pin; hovering there shows no highlight.

  • Selectors are resolved at click-time, so the restriction tracks DOM changes and SPA navigation.
  • It fails closed: if a restriction is configured but none of the containers currently exist in the DOM, commenting is blocked everywhere until one appears.
  • The restriction also governs moving existing pins — a pin dragged outside the allowed area reverts.
  • Blank strings and syntactically-invalid selectors are ignored (with a console warning); if nothing usable remains, commenting stays unrestricted.

When a restriction is active, commenting mode also gives visual feedback so users can see where they may comment:

  • Spotlight — the page outside the allowed container(s) is dimmed (and lightly blurred where the browser supports backdrop-filter), leaving the commentable zone(s) bright and crisp. The effect tracks scrolling/resizing and updates live.
  • Cursor — the pin cursor is shown over commentable areas and a not-allowed cursor over everything else (paired with the snackbar hint on a blocked click).

(With no restriction configured, neither affordance appears — commenting mode looks exactly as before.)

Events

Subscribe with markup.on(event, handler); the returned function unsubscribes. Use markup.off(event, handler) to remove a specific listener, or markup.once(event, handler) to auto-unsubscribe after the first emission.

| Event | Payload | | -------------------------- | ----------------------------------------------------- | | commenting:start | undefined | | commenting:stop | undefined | | pin:placed | PinPlacedData | | pin:cancelled | undefined | | thread:open | {threadId: string} | | thread:close | undefined | | thread:priority-changed | {threadId: string; priority: ThreadPriority \| null} | | comment:reply | {threadId: string; message: string} | | comment:resolve | {threadId: string} | | comment:unresolve | {threadId: string} | | comment:delete | {threadId: string} | | comment:reply:delete | {threadId: string; replyId: string} | | comment:edit | {threadId: string} | | comment:reply:edit | {threadId: string; replyId: string} |

const offOpen = markup.on("thread:open", ({threadId}) => {
    analytics.track("markup_thread_open", {threadId});
});

const offPlaced = markup.on("pin:placed", (data) => {
    console.log("pin at", data.elementPath, data.offsetXPercent, data.offsetYPercent);
});

// Later
offOpen();
offPlaced();

Notifications

The /ui entry exports a notify snackbar helper (it is not available on the headless entry):

import {notify} from "@ceros-dev/markup-sdk/ui";

notify.success("Comment posted");
notify.error("Couldn't load comments.", {description: err.message});

notify.success / info / warning / error(message, opts?) render an in-overlay snackbar with auto-dismiss (4s / 4s / 6s / 8s by default). NotifyOptions accepts description, an action ({label, onClick}), timeoutMs, and dismissible. The SDK uses these internally to surface load, upload, and pin-move failures.

User identity

Once a user is signed in, their name, avatar, and derived initials appear in the toolbar. Identity comes from the decoded JWT (token mode) or from the MarkUp sign-in response (SDK-managed mode).

Single-page-app navigation

The UI build tracks client-side route changes automatically: render() patches history.pushState / replaceState and listens for popstate, re-rendering the comment pins that belong to the new path. Pins are page-scoped — only threads whose page matches the current URL are shown. destroy() restores the original history methods.

Content Security Policy

The SDK renders its UI inside a shadow DOM and injects its styles as inline <style> elements. If your page sets a strict Content-Security-Policy, allow inline styles so the SDK renders correctly:

style-src 'self' 'unsafe-inline';

The SDK uses no eval or inline <script>, so no script-src relaxation is needed for it. When loading the browser bundle from the CDN, allow the CDN host in script-src (e.g. https://sdk.markup.io).

Session persistence & security

By default the resolved session is held in memory only (sessionStorage: "memory"). The "sessionStorage" and "localStorage" modes persist the session token where any script on your page can read it — enable them only when you need the session to survive reloads, and only on trusted origins.

Browser Support

  • Chrome 90+
  • Firefox 88+
  • Safari 14+
  • Edge 90+

License

BSD 3-Clause — see LICENSE.