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

@michelangelo-land/sdk

v0.7.0

Published

Official Michelangelo Land SDK — dynamic OAuth 2.1 + PKCE auth with in-memory tokens, typed client generated from the OpenAPI contract (api.michelangelo.land/v1/openapi.json).

Readme

@michelangelo-land/sdk

Official Michelangelo SDK (TypeScript, zero runtime dependencies).

Meeting point with the API: the swagger — GET /v1/openapi.json (vendored at openapi/openapi.json). src/config.ts, src/types.ts and src/client.ts track it; npm run contract fails if they drift. Interactive reference: GET /v1/docs (Scalar).

Dynamic auth

The SDK is born anonymous and logs in via OAuth 2.1 + PKCE against auth.michelangelo.land. Tokens live only in memory and are applied to every subsequent call automatically.

import { MichelangeloClient } from "@michelangelo-land/sdk";

const client = new MichelangeloClient(); // anonymous
await client.health(); // public — works now

// 1. register once (DCR) — reuse the id via MICHELANGELO_CLIENT_ID afterwards
await client.auth.registerClient({
  client_name: "My App",
  redirect_uris: ["https://myapp.example.com/callback"],
});

// 2. browser step — open `url`, get back `?code=…` on your redirect_uri
const { url, codeVerifier } = await client.auth.beginLogin({
  redirectUri: "https://myapp.example.com/callback",
});

// 3. exchange — from here the token is in memory
await client.auth.exchangeCode({ code, codeVerifier, redirectUri });
await client.whoami(); // Authorization: Bearer … injected
client.auth.logout(); // back to anonymous

Node scripts can skip the manual steps:

import { loginWithLoopback } from "@michelangelo-land/sdk/node-loopback";
await loginWithLoopback(client); // ephemeral 127.0.0.1 server + browser + exchange
npm run example:node-login

Endpoints (swagger → SDK)

| Tag | Swagger (operationId) | SDK | Auth | |---|---|---|---| | auth | getHealth GET /health | client.health() | none (anonymous OK) | | auth | getWhoami GET /whoami | client.whoami() | Bearer required | | users | getMe GET /me | client.me(["username", "email"]) (sparse fieldsets) | Bearer required | | users | updateMe PATCH /me | client.updateMe({ username }) | Bearer required | | users | deleteMe DELETE /me | client.deleteMe() → void (204) | Bearer required | | users | uploadMyAvatar POST /me/avatar (multipart file) | client.uploadMyAvatar(fileOrFormData) → { avatar_url } | Bearer required | | users | getPublicProfile GET /users/{userId} | client.getPublicProfile(userId) → { id, username, avatar_url, is_supporter, … } | Bearer required | | jobs | createJob POST /jobs | client.createJob() → 202 row (200 on idempotent replay) | Bearer required | | jobs | uploadJobAttachments POST /jobs/attachments (multipart, ≤3 images) | client.uploadJobAttachments({ files, idempotencyKey }) or FormData | Bearer required | | jobs | getJob GET /jobs/{jobId} | client.getJob() / client.waitForJob() | Bearer required | | projects | listProjects GET /projects | client.listProjects({ visibility: "all", q }) / client.listAllProjects() | Bearer required | | projects | createProject POST /projects | client.createProject({ name }) → 201 | Bearer required | | projects | getProject GET /projects/{id} | client.getProject() | Bearer required | | projects | updateProject PATCH /projects/{id} | client.updateProject(id, { name, shared }) (icon is server-managed via …/icon) | Bearer required | | projects | deleteProject DELETE /projects/{id} | client.deleteProject() → void (204) | Bearer required | | projects | listProjectFiles GET /projects/{id}/files | client.listProjectFiles(id, { type: "CODE" }) | Bearer required | | projects | saveProjectFiles PUT /projects/{id}/files | client.saveProjectFiles(id, { files }) | Bearer required | | projects | generateProjectIcon POST /projects/{id}/icon | client.generateProjectIcon() → { icon_url } | Bearer required | | projects | pushProjectToGithub POST /projects/{id}/pushes | client.pushProjectToGithub(id) → { pushed, commit_sha } (sync, no polling) | Bearer required | | projects | reshareProject POST /projects/{id}/reshares | client.reshareProject(id, { platform }) | Bearer required | | projects | listPreviewErrors GET /projects/{id}/preview-errors | client.listPreviewErrors(id) (pure read) | Bearer required | | projects | drainPreviewErrors DELETE /projects/{id}/preview-errors | client.drainPreviewErrors(id) → { deleted_count } | Bearer required | | projects | getProjectSupabaseLink GET /projects/{id}/supabase-link | client.getProjectSupabaseLink() | Bearer required | | moderation | createReport POST /reports | client.createReport() → 201 | Bearer required | | moderation | blockUser POST /blocks | client.blockUser({ blocked_id }) | Bearer required | | github | exchangeGithubToken POST /github/token | client.exchangeGithubToken({ code }) | Bearer required | | github | listGithubInstallations / saveGithubInstallation | client.listGithubInstallations() / client.saveGithubInstallation({ id, ghu }) | Bearer required | | github | listGithubRepositories / linkGithubRepository | client.listGithubRepositories() / client.linkGithubRepository() | Bearer required | | github | unlinkGithubRepository DELETE /github/repositories/{projectId} | client.unlinkGithubRepository(project_id) → void (204) | Bearer required | | integrations | createSupabaseAuthorization POST /integrations/supabase/authorizations | client.createSupabaseAuthorization({ code_challenge }) → open authorize_url | Bearer required | | integrations | listSupabaseProjects GET /integrations/supabase/projects | client.listSupabaseProjects() | Bearer required | | integrations | linkSupabaseProject POST /integrations/supabase/links | client.linkSupabaseProject({ project_id, supabase_ref }) | Bearer required | | integrations | unlinkSupabaseProject DELETE /integrations/supabase/links/{projectId} | client.unlinkSupabaseProject(project_id) → void (204) | Bearer required | | integrations | getSupabaseConnection GET /integrations/supabase/connection | client.getSupabaseConnection() | Bearer required | | wallets | getMyWallet GET /wallets/me | client.getMyWallet() | Bearer required | | wallets | listMyTransactions GET /wallets/me/transactions | client.listMyTransactions() / client.listAllMyTransactions() | Bearer required | | usage | getUsageSummary GET /usage/summary | client.getUsageSummary({ period: "month" }) | Bearer required | | usage | getUsageContributions GET /usage/contributions | client.getUsageContributions({ start_date, end_date }) | Bearer required | | usage | getUsageInsights GET /usage/insights | client.getUsageInsights({ timezone }) | Bearer required | | notifications | listNotifications GET /notifications | client.listNotifications() | Bearer required | | notifications | getUnreadCount GET /notifications/unread-count | client.getUnreadCount() | Bearer required | | notifications | markNotificationRead PATCH /notifications/{id} | client.markNotificationRead(id) | Bearer required | | notifications | markAllNotificationsRead PATCH /notifications | client.markAllNotificationsRead() → { updated_count } | Bearer required | | notifications | registerPushToken POST /push-tokens | client.registerPushToken({ token, device_type }) → 201 | Bearer required | | notifications | removePushToken DELETE /push-tokens/{token} | client.removePushToken(token) → void (204) | Bearer required | | billing | createBillingCheckout POST /billing/checkouts | client.createBillingCheckout() → open checkout_url | Bearer required |

Migration notes (0.7.0): GET /explore/projects was removed — community search is listProjects({ visibility: "all", q }) (listExploreProjects* remain as deprecated shims). POST /integrations/supabase/login was renamed to POST /integrations/supabase/authorizations (createSupabaseAuthorization(); startSupabaseOAuth() still works as a deprecated alias). POST /jobs is prompt-only — GitHub pushes are the sync pushProjectToGithub() (POST …/pushes), not a queued job. PATCH /projects/{id} no longer accepts icon_url, GET …/preview-errors lost ?consume=true (use drainPreviewErrors()), POST …/reshare is now POST …/reshares, and the three DELETEs take a path id (/github/repositories/{id}, /integrations/supabase/links/{id}, /push-tokens/{token}) instead of a query/body. Job input appName is now app_name (both accepted by the client types, app_name is canonical).

Not covered on purpose: getOpenApiDocument/getDocs (meta) and handleBillingWebhook (Stripe-signed server hook) / handleGithubWebhook (HMAC-signed server hook).

// Async generation: create, then wait (backoff 2s → 30s, 10 min timeout).
// createJob throws 402 insufficient_credits when the overdraft floor is reached.
const job = await client.createJob({ type: "prompt", input: { prompt: "a timer app" } });
const done = await client.waitForJob(job.id, {
  onProgress: (j) => console.log(j.status),
});
if (done.status !== "succeeded") throw new Error(done.error?.message ?? done.status);

// GitHub push (project must be linked first) — sync, no polling:
await client.linkGithubRepository({ project_id: 12, github_installation_id: 34 });
const push = await client.pushProjectToGithub(12);
if (!push.pushed) console.log("nothing to push", push.commit_sha);

// Prompt with images: upload first, then attach the manifest.
const manifest = await client.uploadJobAttachments({ files: [imageBlob] });
await client.createJob({ type: "prompt", input: { prompt: "redesign this" }, attachments: manifest.attachments });

// Prompt with Live Activity + linked Supabase ref (folded into `input` server-side):
await client.createJob({
  type: "prompt",
  input: { prompt: "add auth" },
  live_activity_token,
  live_activity_user,
  supabase_project_ref: "abcxyz",
});

// Author of a project (community cards show the `author` embed; `user_id` for the full hero):
const author = project.author; // { username, avatar_url } | null
const hero = await client.getPublicProfile(project.user_id!);

// Community feed: every page, one array (`visibility: "all"` + `q` search).
const projects = await client.listAllProjects({ visibility: "all" });
const feed = await client.listAllProjects({ visibility: "all", q: "timer" });

// Files round-trip + reshare + icon:
await client.saveProjectFiles(12, { files: [{ path: "App.tsx", contents: "...", type: "CODE" }] });
await client.reshareProject(12, { platform: "x" });
await client.generateProjectIcon(12);

// Wallet + usage + notifications (money in EUR-cent credits: 1 EUR = 100 credits,
// render display_* verbatim — never divide balance yourself):
const wallet = await client.getMyWallet();
console.log(wallet.display_total); // e.g. "10,10 €"
const summary = await client.getUsageSummary({ period: "month" });
const { unread_count } = await client.getUnreadCount();
await client.registerPushToken({ token: expoPushToken, device_type: "ios" });

// Top-up: 1 EUR = 100 credits (1 credit = 1 EUR-cent), crediting happens in
// the webhook once paid. Small overdraft allowed: POST /jobs returns
// 402 insufficient_credits only once the floor is reached.
const co = await client.createBillingCheckout({ amount_eur: 20 }); // → 2000 credits
openBrowser(co.checkout_url);

Multipart on React Native: Blob construction differs on Hermes — pass a prebuilt FormData instead (e.g. form.append("file", { uri, name, type }) for avatars, form.append("files", …) + optional idempotency_key for job attachments). On Node/web a Blob/File (or FormData) works directly.

Token expiry

Calls that fail with 401 refresh the access token from the in-memory refresh token and retry once (disable with new MichelangeloClient({ autoRefresh: false })). If the refresh fails too, the SDK drops to anonymous and the 401 surfaces — call loginWithLoopback() (or the browser flow) again.

Expo / React Native (Hermes)

Hermes has no crypto.subtle, so inject the expo-crypto backend once at startup — the app already depends on expo-crypto:

import { digest, getRandomBytes, CryptoDigestAlgorithm } from "expo-crypto";
import { setPkceCrypto } from "@michelangelo-land/sdk";

setPkceCrypto({
  randomBytes: (size) => getRandomBytes(size),
  sha256: async (data) => {
    const copy = new Uint8Array(data.byteLength);
    copy.set(data);
    return new Uint8Array(await digest(CryptoDigestAlgorithm.SHA256, copy));
  },
});

(digest accepts a BufferSource; a Uint8Array qualifies.) Alternatively pass pkceCrypto per-client via new MichelangeloClient({ pkceCrypto }). Do not import @michelangelo-land/sdk/node-loopback in app bundles — it needs node:http and is excluded from the Metro graph as long as it stays unimported (the react-native export condition maps the core entry only).

Scripts

  • npm run sync:openapi — vendor the live swagger to openapi/openapi.json
  • npm run contract — fail if SDK defaults drift from the live swagger (run in CI)
  • npm run type-check / npm run build / npm test

Publishing (trusted publishing, no npm token)

Releases are cut by pushing a tag — .github/workflows/release.yml runs type-check, tests, contract check and npm publish --provenance:

npm version 0.3.0 && git push origin main v0.3.0

One-time bootstrap (npm org owner, on npmjs.com): open the @michelangelo-land/sdk package → Settings → Trusted Publisher → add repo michelangelo-land/sdk, workflow release.yml. The very first version must be published once by hand (npm publish --access public from an org member — trusted publishing attaches to an existing package); every later release is tag-driven with no long-lived token.