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

artifakt-sdk

v0.3.1

Published

Embed the Artifakt review surface — anchored comments, versions, approvals — in any web app. Includes the write-only capture snippet for pinning feedback on your app's own UI.

Downloads

96

Readme

artifakt-sdk

Embed the Artifakt review surface — anchored comments, versions, approvals — in any web app. The SDK is a thin client of a Artifakt backend (hosted or self-hosted): your app mounts the review surface for one artifact; every comment, revision, and signature lands in the Artifakt record, and connected agents pick feedback up over MCP exactly as they do for the hosted viewer.

Setup

  1. In Artifakt Settings → Embeddable SDK: enable the SDK to get your publishable key (gvk_…) and register the origins that may embed (e.g. https://app.example.com).
  2. npm install artifakt-sdk

React

import { ArtifaktArtifact } from "artifakt-sdk/react";

<ArtifaktArtifact
  baseUrl="https://app.artifakthq.com"
  artifactId="<artifact id>"
  publishableKey="gvk_…"
  user={{ email: "[email protected]", name: "Dana" }}
  onEvent={(e) => console.log("artifakt:", e.type)}
  style={{ height: 720 }}
/>

Vanilla

import { mountArtifaktArtifact } from "artifakt-sdk";

const embed = await mountArtifaktArtifact(document.getElementById("review"), {
  baseUrl: "https://app.artifakthq.com",
  publishableKey: "gvk_…",
  artifactId: "<artifact id>",
  user: { email: "[email protected]" },
});
// later: embed.destroy()

Server-side token exchange (recommended)

The publishable key is client-safe, but exchanging it server-side keeps your origin allowlist tight and lets you assert the reviewer's identity from your own session:

import { createEmbedToken } from "artifakt-sdk";

// in your backend route
const { token } = await createEmbedToken({
  baseUrl: "https://app.artifakthq.com",
  publishableKey: process.env.ARTIFAKT_PUBLISHABLE_KEY,
  artifactId,
  user: { email: session.user.email, name: session.user.name },
});
// pass `token` to <ArtifaktArtifact token={token} …> instead of publishableKey

Notes

  • Embed tokens are short-lived (1h) and scoped to one artifact; workspace surfaces (artifact list, settings, tokens) are never reachable from an embed.
  • The reviewer identity is host-asserted: Artifakt records what your app vouches. Signatures made through an embed are named accordingly.
  • Rotating the publishable key in Settings invalidates all outstanding embeds.

Capture mode (artifakt-sdk/capture)

A separate, write-only snippet for pinning feedback directly onto your own app's live UI — not an Artifakt-hosted artifact.

npm install artifakt-sdk
import { init } from "artifakt-sdk/capture";

const capture = init({
  baseUrl: "https://app.artifakthq.com",
  publishableKey: "gvk_…",
  surfaceKey: "marketing-deck",   // stable id for this surface
  versionLabel: BUILD_ID,         // recommended: your build/deploy id
  user: { email: "[email protected]", name: "Dana" },
});
// later: capture.destroy();

The workspace's capture policy is either anonymous (identity is host-asserted, like user above) or invite (reviewers verify their email via a magic link before they can leave feedback). Either way, the token this snippet holds is write-only: it can observe versions of and comment on exactly the one surfaceKey it was minted for, and nothing else in the workspace. See Capture mode for policies, snapshots, and token lifetimes.

Interaction model

The widget shows a floating spark (the fab). Clicking it is the lid: it opens and closes an expanded panel. Inside the panel a two-way segment picks the mode — Interactive (the page stays fully usable) or Feedback (a capture overlay turns on and the cursor becomes a ghost pin carrying the next note's number; click the page to plant a note, and a composer opens beside it). Your sent notes list in the panel, each with a numbered pin echoed on the page. Appearance (Auto / Light / Dark — Auto follows the host's prefers-color-scheme) lives behind the panel's ⋯ overflow; the widget is always its own graphite glass, only its edge adapts to the host.

Give interactive elements a stable data-afk id (e.g. <button data-afk="submit-cta">) so pins keep pointing at the right element across re-renders — agents generating UI can do this automatically.

App-anchored pins (canvas/WebGL)

For apps that render with canvas, WebGL, or a PDF viewer, there's no DOM element under the cursor worth pointing at — data-afk has nothing to attach to. Pass anchorProvider to init() and let the app own the anchor in its own scene semantics instead:

const capture = init({
  baseUrl: "https://app.artifakthq.com",
  publishableKey: "gvk_…",
  surfaceKey: "3d-viewer",
  anchorProvider: {
    // Called at pin time, before DOM anchoring. Return your app's own ref
    // (a scene/node id, world coordinates — whatever makes sense) to own
    // the anchor, or null to decline the click and let DOM anchoring
    // handle it (e.g. a click that landed on your app's chrome, not the
    // canvas itself).
    resolve(x, y, target) {
      const hit = myScene.pick(x, y);
      return hit ? { ref: hit.nodeId, data: { worldPos: hit.point } } : null;
    },
    // Called every tracking frame for app-anchored pins. Return the
    // anchor's current viewport coordinates and whether it's presentable
    // right now (in frame, not occluded by your own UI, etc.) — return
    // null when you can't say, which hides the marker either way.
    project(ref, data) {
      const node = myScene.get(ref);
      if (!node) return null;
      const p = myScene.projectToScreen(data.worldPos);
      return { x: p.x, y: p.y, visible: node.visible };
    },
  },
});

The stored anchor records your ref/data verbatim (Artifakt never interprets them) and is round-tripped back to project() on every load — so markers keep tracking a rotating model or a panning canvas the same way DOM anchors track a scrolling page. If no anchorProvider is registered on a later load (or project() returns null/throws), an app-anchored pin's marker simply stays hidden — it's still listed in "my pins" and labeled app-anchored there, it just has nowhere to draw a dot.

View-anchored navigation (single-URL multi-view apps)

For slide decks, wizards, and tab shells that switch what's on screen in JavaScript without ever changing the URL, route matching can't tell one view from another — a pin made on a hidden view would list under "This page" but clicking it had nowhere to jump to. Pass viewProvider to init() so the app can name and switch views itself:

const capture = init({
  baseUrl: "https://app.artifakthq.com",
  publishableKey: "gvk_…",
  surfaceKey: "sales-deck",
  viewProvider: {
    // The id of the view showing right now (stamped on each pin's anchor
    // at capture time), or null when none applies.
    getView: () => `slide-${deck.currentSlide}`,
    // Switch the app to `view`. Return a promise if the switch animates
    // (e.g. a fade) — the widget waits for it before scrolling/flashing.
    showView: (view) => deck.goToSlide(Number(view.replace("slide-", ""))),
    // Optional: prettifies a view id for the echo list's group headings.
    label: (view) => `Slide ${view.replace("slide-", "")}`,
  },
});

Once registered, a pin captured on a hidden view groups under that view's label in the "my pins" list instead of a generic "This page" bucket, and clicking it calls showView before scrolling/flashing the marker. Without a viewProvider, the widget still fails honestly: jumping to a pin whose element exists but isn't currently shown reports "This pin is on a view that isn't showing right now" instead of silently doing nothing.

Already syncing your view to location.hash? (>= 0.2.4) Apps that do — decks, wizards, docs sites — hand-write the same provider every time, so pass the string "hash" instead of the object: viewProvider: "hash" reads location.hash (minus the #) for getView and assigns it for showView. One caveat: a view is only recorded while the hash is actually set, so if your app can start with an empty hash, put the initial view there too (e.g. a history.replaceState on load) or its first-view pins won't carry a view id.

Want the "hash" preset AND a label? (>= 0.2.5) The bare string is sugar for the whole provider, so it couldn't carry one — pass the preset as an object instead:

viewProvider: { preset: "hash", label: (view) => `Slide ${view.replace("slide-", "")}` }

Same hash-synced getView/showView as "hash", plus your label hook. Orthogonality, not a special case: how views are read/switched is mechanical and presettable, what they're called is semantic and hookable — now you get both independently.

Automatic view titles (>= 0.2.5): you may not need label at all. Every pin captured on a registered view records that view's own visible heading (the first on-screen, unoccluded h1/h2/h3) alongside the view id, frozen at capture time — a review record describes what the reviewer actually saw, even if the heading is reworded later. Display precedence everywhere a view is shown: an explicit label() hook wins if you supplied one, then the captured heading, then the raw view id. Reach for label only when the heading text itself isn't a good group name.

If a pin jump ever appears to do nothing, pass init({ debug: true }) — it also narrates the pin-jump path to the console (prefix [artifakt-capture]), so you can see exactly where the jump landed and why.

No bundler? The dist files are dependency-free native browser ESM: vendor dist/capture.js + dist/capture-anchor.js side by side and import { init } from "./capture.js" from a <script type="module">. Put them in a version-named directory (assets/artifakt-0.1.4/) and change the directory on upgrade — CDN/browser caches outlive republishes, so same URL + new bytes silently keeps old code running; new content needs a new URL. For pipelines that build artifacts (e.g. decks generated per publish), inject the snippet at build time so every output carries it with the current build id.

Local pin echo (>= 0.1.6): the widget remembers, in this browser's localStorage only, the pins THIS browser has successfully sent (capped at 100, oldest evicted first) — never read back from the server, so it stays a write-only client. Idle, it shows a small count badge on the ✎ button and a numbered marker at each pin's anchor position (skipped if the anchor no longer resolves — the page changed — but the pin still shows in the list). Markers position on the exact clicked element; if a page change breaks that selector, they fall back to the data-afk anchor's own recorded geometry (0.1.8+) rather than misapplying the clicked element's position to it. A selector match is also verified against the pin's recorded text before being trusted (0.2.0), so a virtualized/recycled node that now matches the same selector but holds different content falls back to the data-afk geometry too, instead of misplacing the marker. Clicking a marker or the badge opens a panel: the pin's text, route, and timestamp, capped honestly at "Sent to the workspace ✓" (there's no read path, so it can never say "seen" or "resolved"), plus "Remove from this view" — which deletes only the local echo, never the comment in Artifakt. The badge's list (0.2.1) groups entries by page — this page's pins first, then the rest under their own route — and every entry jumps to its pin, navigating across pages when needed; clicking a route heading (0.2.2) jumps to that page too. The list links to the full record in Artifakt (0.2.2). Markers track their elements through transforms/slide navigation (a frame-loop runs while markers are on screen) and hide when the element isn't on the current page/viewport, isn't actually rendered (opacity/visibility/ display), or is covered by another layer — including decks that STACK slides in place and switch via opacity/visibility/z-index rather than moving them off-screen (0.1.9+, via checkVisibility and periodic elementFromPoint occlusion sampling); on a multi-page surface only the current page's markers show, but the list always shows every pin. Marker and panel positions also compensate for a transformed, filtered, or zoomed ancestor around the widget itself (0.2.0, via measurement — not style inspection), so pins still land correctly under a scaled or transformed host layout. This is a personal echo, clearly distinct from the shared RECORD in Artifakt: another browser or device won't show these markers, only your team's Artifakt view does. Verified, synced-across-devices echo is a possible future tier, not this one. Zero new network calls: everything above is localStorage and rendering.

Interaction safety: idle, the widget is one floating button and a single Escape listener that ignores everything outside pin mode — no global click/drag/key interception, so it can't fight your app's own navigation. (The local pin echo above runs a requestAnimationFrame loop, but only while idle with at least one route-matching stored pin and the tab visible, and only to reposition/hide markers — it never calls preventDefault/stopPropagation, touches only the widget's own marker nodes, and pauses the moment the tab is hidden.) Keystrokes typed into the widget's panels stop at the widget boundary, so apps that bind Space/arrows (slide decks) don't react while a reviewer types. Pin mode is deliberately modal until a pin is placed or exited. While a panel or overlay is open (pin, compose, the identity/verify prompts, or the pin echo panel), a capture-phase listener keeps keys from reaching the page even when focus has drifted outside the widget entirely (a bare pin overlay, or a click that lands on a panel's padding) — Escape still exits, and typing is redirected back into the open panel. On failure (bad key, unlisted origin, server unreachable) it disables itself with one console warning and never breaks the host page. Pass expectedVersion (the version your build vendored) to init() to get a one-time console warning if a stale cached copy of the widget ends up loaded on the page.

Testing

The capture widget's browser behaviors (pinning, echo markers, occlusion gating, layered anchors) are covered by a Playwright harness — an "element zoo" fixture with one of every element/rendering shape, exercised against a fully mocked API:

npm i && npx playwright install chromium && npm run test:zoo

See test/zoo.spec.ts and test/fixtures/zoo.html.

Backoff and update awareness (>= 0.1.4): if the token exchange comes back 403 three times in a row (never counting the invite flow's verification_required, and never a network error), the widget assumes the key is dead, goes quiet for 24h — no render, no network calls at all, just one console warning — and tries again normally afterward; a single success at any point clears the counter. Separately, every observe response carries the server's latest published SDK version; if it's newer than the one you're running, the widget logs one console note and adds a small dismissible line inside its own panels ("SDK x.y.z available — owners: ask your agent to upgrade") — never a page-load banner, and dismissing it stays quiet until an even newer version ships. Nothing auto-updates; upgrading is always an owner's call.