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

harbor-lookup

v0.1.0

Published

Give it one social account, get back the other accounts verifiably linked to it — a focused TypeScript client for Harbor's Polycentric verification lookups.

Readme

harbor-lookup

Give it one social account, get back the other accounts verifiably linked to it.

import { Harbor } from 'harbor-lookup';

const harbor = new Harbor();
const socials = await harbor.linkedSocials({ platform: 'youtube', account: '@asphaltstorm96' });

for (const social of socials) {
  console.log(social.platformName, social.account, social.url);
}
// YouTube  @asphaltstorm96   https://www.youtube.com/@asphaltstorm96
// YouTube  @shadowstorm_96   https://www.youtube.com/@shadowstorm_96
// Twitch   asphaltstorm96    https://www.twitch.tv/asphaltstorm96
// Twitch   shadowstorm0896   https://www.twitch.tv/shadowstorm0896
// X        asphaltstorm96    https://x.com/asphaltstorm96
// X        shadowstorm_96    https://x.com/shadowstorm_96
npm install harbor-lookup

Zero configuration: it talks to Harbor's public servers and trusts Harbor's verifier by default. No key, no account, no sign-up.

What this actually tells you

Harbor is a client for Polycentric, a federated identity network. People publish a claim ("this YouTube channel is mine"), a verifier checks it by looking for a one-time token in the account's own bio or channel description, and publishes a signed verification. This package runs that index backwards: from one claimed account to the identity that claims it, and from there to everything else that identity has had verified.

So a result is not "these accounts look similar" or "these usernames match". It is "the person who controls this YouTube channel also demonstrated control of these, and a verifier you named signed off on it". That is a much stronger claim than username enumeration, and a much rarer one.

[!IMPORTANT] Most lookups return nothing, and that is the expected answer. Harbor's dataset is opt-in and small. An arbitrary YouTube channel is almost certainly not in it. Treat this as enrichment that is occasionally very good, never as a resolver you can depend on.

The API

const harbor = new Harbor(options?);

| Method | Returns | | :-------------------------------- | :-------------------------------------------------------------------------------- | | linkedSocials(query, opts?) | Social[] — every verified account on the identities that claim the queried one. | | linkedIdentities(query, opts?) | The same, grouped by identity, for when two identities claim the same account. | | resolveIdentities(query, opts?) | string[] — just the identities, when that is all you need. | | socialsOf(identity, opts?) | Social[] — every verified account one identity claims. |

A query is a platform plus whichever identifier that platform's claims record:

await harbor.linkedSocials({ platform: 'youtube', accountId: 'UC1-QO9dEJxK05SAEM02bxkQ' });
await harbor.linkedSocials({ platform: 'twitch', account: 'asphaltstorm96' });
await harbor.linkedSocials({ platform: 'website', fields: { url: 'https://example.com' } });

Matching is containment: a claim matches when it holds every pair you gave, and may carry more. A claim missing one of them does not match — so accountId finds nothing on a platform whose claims only record a handle. PLATFORM_SLUGS lists the platforms Harbor's verifier has a route for (x, youtube, github, discord, hacker-news, rumble, twitch, website), and the field accepts a slug added after this release.

The account you queried comes back too — it is one of the identity's claims like any other. Filter it out if you only want the others.

Options

new Harbor({
  servers: ['https://srv.harbor.social'], // defaults to Harbor's published seed servers
  verifiers: [HARBOR_VERIFIER_IDENTITY], // whose signature counts as verification
  fetch: myFetch, // for a proxy, instrumentation, or a test
  format: 'binary', // 'text' (default) is what a browser needs
});

await harbor.linkedSocials(query, { signal: AbortSignal.timeout(5000), allSchemas: true });

verifiers is the security-relevant one. A Polycentric claim is just an assertion until somebody signs a verification of it, and ListVerificationClaims — the second half of the lookup — takes no verifier argument and will happily return an identity's unverified claims. This package filters them out against verifiers on both halves. Set it to a verifier you trust, or leave it as Harbor's.

Errors

HarborError is the base. RpcError carries the gRPC code and the server that answered; TransportError means no answer at all; QueryError rejects a query that cannot mean anything (an empty one would match every verified claim there is); DecodeError means the response was not the shape the schema says — a protocol mismatch rather than a failed lookup.

Servers are tried in order, and a second one is only asked when the first failed to serve the request. NOT_FOUND from one federated server is NOT_FOUND from all of them.

What a Social is

interface Social {
  platform: PlatformSlug; // 'youtube'
  platformName: string; // 'YouTube'
  account?: string; // '@asphaltstorm96'
  accountId?: string; // 'UC1-QO9dEJxK05SAEM02bxkQ' — where the platform has stable ids
  url?: string; // the url the claim recorded; not built here
  identity: string; // the Polycentric identity that claims it
  verifiedBy: string[]; // which of your trust roots signed it
  fields: Record<string, string>; // every field the claim carries
}

Read fields when you need something the named properties do not cover. A verification schema is defined per claim by whoever makes it, so the fields vary: YouTube claims carry platform / account / account_id / url, while X claims carry no id at all. The named properties are the common ones, not the whole claim.

Caveats worth reading before you build on this

  • Coverage. Said above, worth repeating: usually empty.
  • Schemas are per-claim and user-definable. There is no guarantee about which fields exist. Harbor's own schema helper carries a TODO noting that re-serializing a schema does not yet produce canonical bytes, which is why this package does not filter by schema_digest — digests only group claims across clients when the claimants pinned identical bytes.
  • Verification means "a verifier found a token in the bio", which proves control of the account at the time it was checked. It does not prove the account was not later sold, nor that the two accounts belong to the same human rather than the same team.
  • gRPC, not REST. Polycentric v2 has no JSON transcoding; the old v1 REST paths (find_claim_and_vouch and friends) are gone from these servers. This package speaks grpc-web over fetch, so it works in a browser and in Node 20+ with nothing native.

Browser use

Works as-is, with no grpc-web proxy in front. srv.harbor.social answers the preflight with access-control-allow-origin: * and allows POST with content-type, x-grpc-web, grpc-timeout, authorization, so a page on any origin can call it. Both framings were checked against the live server: the default format: 'text' base64-encodes frames and carries the gRPC status in the body, and 'binary' works too. Nothing in the package touches node: modules at runtime.

Where the types come from

proto/ holds the polycentric.v2 definitions, vendored from futo-org/Harbor at a pinned commit, and src/generated/ is protoc-gen-ts output from them — the same generator Harbor's own js-core uses, so these types are not a second dialect of the same schema. Refresh with ./tools/fetch-protos.sh develop and npm run generate, then read the diff.

The whole schema is generated, not just the two calls this package wraps, so a consumer who needs another Polycentric service can build a client from v2 and VerificationsServiceClient rather than vendoring the protos again. The package is sideEffects: false, so a bundler drops what you do not import.

Why not the official client?

@polycentric/js-core exists in the Harbor monorepo, with js-node and js-browser on top. Two reasons it is not used here: none of them are published to npm (all 0.0.0, workspace:*), and js-node's createPolycentricNodeClient wants sqlite or postgres, a blob directory and a wasm core before it will start, because it is a full replicating node. This package makes two unary calls and keeps no state. If you need to publish events rather than read them, use theirs.

Development

npm install
npm run check        # format → lint → typecheck → test → build
npm test             # the offline suite — replayed fixtures, no network
npm run test:live    # against srv.harbor.social, read-only

The offline tests replay real grpc-web response bytes recorded from srv.harbor.social into test/fixtures/ by npm run record-fixtures, so they exercise the actual decoder against actual server output. There are no skipped tests in the default run; the live suite is a separate script rather than a suite that skips itself.

License

MIT. The vendored .proto files are Harbor's, under the license in their repository.