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

@evidgo/mini-sdk

v0.5.1

Published

SDK for building Evid-Go mini apps. Wraps the WebView bridge (postMessage RPC) into a promise API.

Readme

@evidgo/mini-sdk

The official SDK for building Evid-Go mini apps — web bundles that run inside the Evid-Go mobile app (in a native WebView) or the web app (in an iframe).

Your mini app calls native capabilities (camera, location, storage) and the org-scoped Evid-Go backend without ever handling a JWT. Every call is proxied to the host over postMessage RPC; the host injects auth + the active X-Org-Id, and enforces a per-app capability allowlist you were granted when your mini app was registered.

import { evid } from "@evidgo/mini-sdk";

await evid.ready();                          // wait for the host handshake
const ctx = await evid.getContext();         // { user, orgId, locale, apiUrl }
const { data: projects } = await evid.projects.list();
const loc = await evid.getLocation();                // needs "location"
const shots = await evid.capture({ mode: "photo" }); // needs "capture"

Install

npm install @evidgo/mini-sdk
# or
pnpm add @evidgo/mini-sdk
# or
yarn add @evidgo/mini-sdk

ESM only. Requires a bundler (Vite, webpack, etc.) — it is meant to ship inside a web app you build and register as an Evid-Go mini app.

Concepts

  • Host — the Evid-Go native app (React Native WebView) or web app (iframe) that loads your mini app. Outside a host (a plain browser tab) evid.isHost() stays false and every proxied call rejects with NO_HOST, so you can guard host-only paths and still run your app standalone during dev.
  • The handshake — the native host injects its globals before your page runs, so isHost()/capabilities() answer immediately there. A cross-origin iframe cannot be script-injected, so on web the host introduces itself over postMessage and those two answer false/[] until it lands. Await evid.ready() before branching on them. evid.api and the typed helpers wait on their own — only synchronous checks need this.
  • Capabilities — each mini app is granted a subset of api | write | location | capture | upload | storage. A call for a capability you weren't granted rejects with a BridgeError (CAPABILITY_DENIED). Read yours with evid.capabilities().
  • Org scope — backend reads/writes are automatically scoped to the user's active organization. You never pass or see a token or org id header.
  • Envelopes — backend helpers return a Strapi { data } envelope. List helpers on core routes also carry meta.pagination.
  • State — Evid-Go stores projects, media and their tags; anything else your app needs to persist is yours to host. evid.storage is device-local (native keychain on mobile, localStorage on web), so back real state with your own backend.

Getting started

import { evid, BridgeError } from "@evidgo/mini-sdk";

async function main() {
  // Resolves true inside either host, false standalone. Do NOT test
  // `evid.isHost()` before this — on web it is not yet known.
  if (!(await evid.ready())) {
    console.warn("Not running inside Evid-Go — host calls will fail.");
    return;
  }

  console.log("granted:", evid.capabilities());
  const { user, orgId, locale, apiUrl } = await evid.getContext();

  try {
    const { data: feed } = await evid.projects.feed({ limit: 20 });
    render(feed);
  } catch (err) {
    if (err instanceof BridgeError && err.code === "CAPABILITY_DENIED") {
      // your app wasn't granted "api"
    } else {
      throw err;
    }
  }
}

Core API

| Member | Capability | Returns | | --- | --- | --- | | evid.isHost() | — | boolean — host known (see the handshake note) | | evid.ready() | — | Promise<boolean> — true once a host introduced itself | | evid.capabilities() | — | Capability[] granted to this app | | evid.getContext() | — | MiniAppContext — { user, orgId, locale, apiUrl } | | evid.resolveUrl(path) | — | absolute url for a relative API path (media, avatars) | | evid.api(path, opts?) | api | proxied org-scoped REST call (parsed JSON) | | evid.getLocation() | location | GeoLocation | | evid.capturePhoto(opts?) | capture | MediaResult (single system-picker shot) | | evid.captureVideo(opts?) | capture | MediaResult | | evid.scanDocument(opts?) | capture | MediaResult (PDF) | | evid.capture(opts?) | capture | MediaResult[] (full-screen camera UI) | | evid.storage.get(key) | storage | string \| null (device-local) | | evid.storage.set(key, value) | storage | void (device-local) |

evid.api(path, { method?, body?, query? }) is the escape hatch for endpoints the typed helpers don't cover. The host validates path against its allowlist, so an unlisted endpoint rejects with PATH_DENIED rather than reaching the backend.

Media urls

Urls in API responses are relative Strapi paths (/uploads/…). The backend origin is whatever the host is talking to and is handed to you as context.apiUrl; never bake one into your bundle with a build-time env var, or the same build breaks between dev, staging and prod.

<img src={evid.resolveUrl(media.file?.url) ?? ""} />

resolveUrl passes absolute and data: urls through untouched, and returns the path unchanged outside a host (nothing to prepend).

Capture

const items = await evid.capture({
  projectId,                                     // attach to a project
  mode: "photo",                                 // photo | video | audio | scan
  tags: [{ key: "task", value: "seal_check" }],  // stamped on each media
});

capturePhoto/captureVideo/scanDocument take { projectId?, tags? } and return one MediaResult; capture opens the host's full-screen camera and resolves with everything captured (empty array if the user backs out).

tags are applied as the media is created, which is how captured evidence stays linked to whatever your app captured it for — no follow-up evid.tags.apply round-trip, and the tag is on the record from the very first version of it. evid.projects.photos(id) returns each media's applied tags flattened to [{ key, value }], so you can read them back in one query.

Each MediaResult carries { documentId, kind, url } plus, when the host knows them, projectId, capturedAt and the capture geotag (latitude/longitude/accuracy).

Capture is a device flow. In the web host all four methods reject with UNSUPPORTED — design a fallback (or hide the entry point) when capabilities() includes capture but you are running framed in the web app.

Typed backend helpers

The api capability unlocks org-scoped, typed helpers over the backend. Prefer these over raw evid.api(path); drop to evid.api for anything not covered.

const { data: feed }     = await evid.projects.feed();
const { data: project }  = await evid.projects.get(id);   // detail + cover
const { data: photos }   = await evid.projects.photos(id); // media + tags
const { data: media }    = await evid.media.photos({ page: 1, pageSize: 20 });
const { data: labels }   = await evid.labels.list();
const { data: trail }    = await evid.custodyLogs.list({ projectId: id });
const { data: members }  = await evid.members.list();
const { data: sub }      = await evid.subscription.get(); // plan + usage
const { data: tags }     = await evid.tags.list(mediaId);
const { data: comments } = await evid.comments.list(mediaId);

Reads (api capability)

| Helper | Endpoint | | --- | --- | | evid.projects.list(query?) | GET /api/projects | | evid.projects.feed(query?) | GET /api/projects/feed | | evid.projects.get(id, query?) | GET /api/projects/:id | | evid.projects.photos(id, query?) | GET /api/projects/:id/photos | | evid.media.list(query?) | GET /api/medias | | evid.media.photos(query?) | GET /api/medias/photos | | evid.media.get(id, query?) | GET /api/medias/:id | | evid.labels.list() | GET /api/labels | | evid.custodyLogs.list({ projectId?, mediaId? }) | GET /api/custody-logs | | evid.miniApps.list() | GET /api/mini-apps | | evid.members.list(query?) | GET /api/memberships | | evid.members.mine() | GET /api/memberships/mine | | evid.userGroups.list(query?) | GET /api/user-groups | | evid.userGroups.get(id) | GET /api/user-groups/:id | | evid.projectGroups.list({ projectId? }) | GET /api/project-groups | | evid.projectGroups.get(id) | GET /api/project-groups/:id | | evid.tags.list(mediaId) | GET /api/media-tags?mediaId= | | evid.tagKeys.list() | GET /api/tag-keys | | evid.comments.list(mediaId, query?) | GET /api/media-comments?mediaId= | | evid.comments.replies(id, query?) | GET /api/media-comments/:id/replies | | evid.projectComments.list(projectId, query?) | GET /api/project-comments?projectId= | | evid.projectComments.replies(id, query?) | GET /api/project-comments/:id/replies | | evid.members.get(id) | GET /api/memberships/:id | | evid.stars.list({ targetType? }) | GET /api/stars | | evid.stars.items(query?) | GET /api/stars/items | | evid.shareLinks.list({ projectId? }) | GET /api/share-links | | evid.shareLinks.view(token) | GET /api/public/share/:token | | evid.exports.list(query?) | GET /api/export-packages | | evid.exports.get(id) | GET /api/export-packages/:id | | evid.exports.estimate(query) | GET /api/export-packages/estimate | | evid.members.inviteInfo(token) | GET /api/memberships/invites/:token | | evid.projects.trash(query?) | GET /api/projects/trash | | evid.media.trash(query?) | GET /api/medias/trash | | evid.map.mediaPoints(query?) | GET /api/medias/map | | evid.map.projectPins() | GET /api/projects/map | | evid.checklist.list(projectId, query?) | GET /api/checklist-items?projectId= | | evid.checklist.all(query?) | GET /api/checklist-items/all | | evid.notifications.list(query?) | GET /api/notifications | | evid.notificationPreferences.get() | GET /api/notification-preferences/mine | | evid.organization.mine() | GET /api/organizations/mine | | evid.support.list() | GET /api/support-requests | | evid.health.get() | GET /api/health | | evid.home.stats() | GET /api/home/stats | | evid.subscription.get() | GET /api/subscription | | evid.subscription.plans() | GET /api/subscription/plans | | evid.subscription.usageHistory(months?) | GET /api/subscription/usage-history | | evid.subscription.billingDetails() | GET /api/subscription/billing-details |

evid.members is the org-scoped user surface. The raw users-permissions /api/users is not reachable — it isn't org-scoped and would leak cross-org. For the same reason only /api/organizations/mine is reachable, not the organizations collection: evid.organization.mine() resolves the active org from X-Org-Id rather than letting a mini app name one.

evid.stars.items() hydrates each star into the same row payload the target's own list renders, so a "starred" screen is one paged read instead of a call per type. Rows are grouped by targetType first, then newest star first.

Downloading an export zip is not in the SDK. GET /api/export-packages/:id/ download is an authenticated binary stream and the mini app holds no token — the host owns that step. Poll evid.exports.get(id) until status === "ready" and hand off from there.

Writes (write capability)

Writes need the separate write capability. Only the exact method+path pairs below are allowed; the backend still enforces the caller's org role, so write never grants privilege the user doesn't already have.

const { data: project } = await evid.projects.create({ name: "Site A" });
const { data: group }   = await evid.projectGroups.create({ name: "Region 1" });
const { data: invited } = await evid.members.invite({ email: "[email protected]" });
const { data: link }    = await evid.shareLinks.create({ project: projectId });
await evid.tags.apply({ media: mediaId, key: "room", value: "kitchen" });
await evid.comments.add({ media: mediaId, body: "Looks good" });
await evid.projectComments.add({ project: projectId, body: "Site walk done" });
await evid.projects.update(projectId, { archived: true }); // archive
await evid.stars.star("project", projectId);

| Helper | Endpoint | | --- | --- | | evid.projects.create(input) | POST /api/projects | | evid.projects.update(id, input) | PUT /api/projects/:id | | evid.projects.remove(id) | DELETE /api/projects/:id | | evid.media.update(id, input) | PUT /api/medias/:id | | evid.media.remove(id) | DELETE /api/medias/:id | | evid.labels.create(input) | POST /api/labels | | evid.labels.update(id, input) | PUT /api/labels/:id | | evid.labels.remove(id) | DELETE /api/labels/:id | | evid.tags.apply(input) | POST /api/media-tags | | evid.tags.update(id, value) | PUT /api/media-tags/:id | | evid.tags.remove(id) | DELETE /api/media-tags/:id | | evid.tagKeys.create(key) | POST /api/tag-keys | | evid.tagKeys.update(id, key) | PUT /api/tag-keys/:id | | evid.tagKeys.remove(id) | DELETE /api/tag-keys/:id | | evid.comments.add(input) | POST /api/media-comments | | evid.comments.edit(id, body) | PUT /api/media-comments/:id | | evid.comments.remove(id) | DELETE /api/media-comments/:id | | evid.projectComments.add(input) | POST /api/project-comments | | evid.projectComments.edit(id, body) | PUT /api/project-comments/:id | | evid.projectComments.remove(id) | DELETE /api/project-comments/:id | | evid.userGroups.create(input) | POST /api/user-groups | | evid.userGroups.update(id, input) | PUT /api/user-groups/:id | | evid.userGroups.remove(id) | DELETE /api/user-groups/:id | | evid.projectGroups.create(input) | POST /api/project-groups | | evid.projectGroups.update(id, input) | PUT /api/project-groups/:id | | evid.projectGroups.remove(id) | DELETE /api/project-groups/:id | | evid.projectGroups.addProject(id, projectId) | POST /api/project-groups/:id/projects/:projectId | | evid.projectGroups.removeProject(id, projectId) | DELETE /api/project-groups/:id/projects/:projectId | | evid.members.invite(input) | POST /api/memberships/invites | | evid.members.update(id, input) | PUT /api/memberships/:id | | evid.members.revoke(id) | DELETE /api/memberships/:id | | evid.members.resendInvite(id) | POST /api/memberships/:id/resend-invite | | evid.members.reactivate(id) | POST /api/memberships/:id/reactivate | | evid.members.adminAccept(id) | POST /api/memberships/:id/accept | | evid.stars.star(type, id) | POST /api/stars/:targetType/:targetId | | evid.stars.unstar(type, id) | DELETE /api/stars/:targetType/:targetId | | evid.shareLinks.create(input) | POST /api/share-links | | evid.shareLinks.revoke(id) | DELETE /api/share-links/:id | | evid.exports.create(input) | POST /api/export-packages | | evid.members.acceptInvite(token) | POST /api/memberships/invites/accept | | evid.projects.restore(id) | POST /api/projects/:id/restore | | evid.projects.removePermanently(id) | DELETE /api/projects/:id/permanent | | evid.media.restore(id) | POST /api/medias/:id/restore | | evid.media.removePermanently(id) | DELETE /api/medias/:id/permanent | | evid.checklist.add(input) | POST /api/checklist-items | | evid.checklist.update(id, patch) | PUT /api/checklist-items/:id | | evid.checklist.remove(id) | DELETE /api/checklist-items/:id | | evid.checklist.reorder(projectId, ids) | PUT /api/checklist-items/reorder | | evid.notifications.markRead(id) | PATCH /api/notifications/:id/read | | evid.notifications.markAllRead() | PATCH /api/notifications/read-all | | evid.notificationPreferences.update(input) | PUT /api/notification-preferences/mine | | evid.organization.update(id, input) | PUT /api/organizations/:id | | evid.support.submit(input) | POST /api/support-requests | | evid.miniApps.create(input) | POST /api/mini-apps | | evid.miniApps.update(id, input) | PUT /api/mini-apps/:id | | evid.miniApps.remove(id) | DELETE /api/mini-apps/:id | | evid.subscription.checkout(plan) | POST /api/subscription/checkout | | evid.subscription.portal() | POST /api/subscription/portal | | evid.subscription.changePlan(plan) | POST /api/subscription/change-plan |

projects.remove and media.remove are soft deletes — the row moves to the trash and stays restorable until the retention window lapses. restore and removePermanently are the two ways back out; the permanent one is owner/admin and refuses anything not trashed first.

The billing writes hand back a Stripe URL for the user to visit; they never move money by themselves, and the backend answers owner-only. changePlan is a dev escape hatch and 404s unless the server runs with ALLOW_MANUAL_PLAN_CHANGE=true. miniApps.create/update/remove grant host capabilities to another app — an admin surface (owner/admin server-side), not a convenience.

A few of these replace rather than patch: userGroups.update takes the whole members set and projectGroups.update the whole projects set. To toggle a single project in a group use addProject/removeProject — they are idempotent and can't race a concurrent edit into dropping the rest.

Evidence itself is never created through evid.api. New media comes from the capture flow (evid.capture, evid.capturePhoto, …), which is what hashes the file, chains the custody anchor and meters the plan quota. evid.media.update edits metadata (caption, description) on a record that already exists.

Error handling

Every call rejects with a BridgeError on failure. Inspect .code:

| .code | Meaning | | --- | --- | | NO_HOST | not running inside an Evid-Go host | | TIMEOUT | the host didn't answer within 30s | | CAPABILITY_DENIED | your mini app wasn't granted that capability | | PATH_DENIED | evid.api path/method is not on the host allowlist | | PERMISSION_DENIED | the user declined a device permission (camera, location) | | CANCELED | the user backed out of a capture or scan | | UNSUPPORTED | device-only method called in the web host | | UNAVAILABLE | the underlying capability is missing on this device | | BAD_PARAMS | a required argument was missing |

Backend failures surface with the API's own status/message.

import { BridgeError } from "@evidgo/mini-sdk";

try {
  await evid.getLocation();
} catch (err) {
  if (err instanceof BridgeError && err.code === "PERMISSION_DENIED") {
    // ask the user to enable location for the Evid-Go app
  }
}

TypeScript

The package ships full types. Import model types alongside evid:

import type {
  Capability,
  MiniAppContext,
  MediaResult,
  CaptureTagInput,
  Project,
  ProjectMedia,
  Subscription,
} from "@evidgo/mini-sdk";

Example

A runnable React + Vite mini app lives in example/. It exercises every bridge method and is the fastest way to see the SDK end to end.

It links the SDK with file:.., which hard-copies dist/. After editing SDK sources: rebuild (pnpm build here), re-run pnpm install in example/, delete example/node_modules/.vite, then restart the dev server — otherwise Vite serves a stale pre-bundle.

License

UNLICENSED — for use with the Evid-Go platform.