@byjp/atproto-deeplink
v0.2.0
Published
Resolve at:// URIs into website/app URIs via the me.byjp.atproto.deeplink lexicons, using microcosm for record & identity lookups.
Downloads
92
Maintainers
Readme
@byjp/atproto-deeplink
A trivial lexicon, and a small, low-dependency TypeScript library, for declaring & resolving default/preferred sites for viewing at:// URIs.
[!WARNING] This is a prototype The lexicons live under
me.byjp.atproto.deeplink.*today. If this gains traction expect them to move/rename.
Lexicons
See lexicons/ for the two lexicon definitions.
Deeplink transform
A deeplink record declares other URIs which are equivalent to at:// URIs with a given NSID.
For example, Bluesky might publish at://atproto-lexicons.bsky.social/me.byjp.atproto.deeplink.transform/app.bsky.feed.post:
{
"$type": "me.byjp.atproto.deeplink.transform",
"uris": ["https://bsky.app/profile/{{did}}/post/{{rkey}}"]
}…declaring that the bsky.app URL (with {{did}} and {{rkey}} substituted) is a great way to view app.bsky.feed.post records.
Because it's hosted in the same atproto account/repo that defines the app.bsky.feed.post lexicon, it's the canonical alternate URI for that NSID.
Deeplink preference
Any account can also state a preference for how it likes to view any NSID with at://<you>/me.byjp.atproto.deeplink.preference/<nsid> records. These point at one or more transform records (which it need not own):
{
"$type": "me.byjp.atproto.deeplink.preference",
"deeplinks": ["at://bluepy.social/me.byjp.atproto.deeplink.transform/app.bsky.feed.post"]
}Template tokens
uris entries are ordered (most preferred first) and support three tokens:
| Token | Value |
| ---------------- | ------------------- |
| {{did}} | the repo DID |
| {{rkey}} | the record key |
| {{collection}} | the collection NSID |
Only did (never handle) is offered, because it's stable by definition.
[!IMPORTANT] A site wanting deeplink support is expected to accept DIDs in its URLs.
Package Usage
import { resolve } from "@byjp/atproto-deeplink";
// Canonical/anonymous resolution
await resolve("at://did:plc:author/app.bsky.feed.post/abc123");
// => "https://bsky.app/profile/did:plc:author/post/abc123"
// Preferred resolution, with canonical fallback
await resolve("at://did:plc:author/app.bsky.feed.post/abc123", { as: "byjp.me" });
// => "https://bluepy.social/p/did:plc:author/abc123"
// Prefer particular scheme(s) among the candidate URIs, if they're present.
await resolve("at://did:plc:author/app.bsky.feed.post/abc123", {
as: "byjp.me",
schemes: ["gemini", "https"],
});
// => "gemini://deepsky.space/p/did:plc:author/abc123"resolve returns null when no suitable URI can be found.
How resolution works
- The
at://URI is parsed; its handle is resolved to a DID via Slingshot (microcosm's record/identity cache), if necessary. - With
as: theasaccount'sme.byjp.atproto.deeplink.preferencerecord for the collection is fetched; each referencedtransformrecord is followed in order and itsuriscollected. - Otherwise / as a fallback: the NSID's lexicon authority DID is resolved via a DNS-over-HTTPS TXT lookup at
_lexicon.<authority>, and its canonicaltransformrecord is read. - The first suitable template (matching
schemes, if given) is chosen and its tokens substituted.
Options
interface ResolveOptions {
as?: string; // handle or DID whose preference to consult first
schemes?: string[]; // acceptable URI schemes, most preferred first
slingshot?: string; // override the Slingshot base URL
doh?: string; // override the DNS-over-HTTPS JSON endpoint
fetch?: typeof fetch; // inject a fetch implementation (tests/custom transport)
}The package has no runtime dependencies and is isomorphic — it uses the global fetch and resolves NSIDs over DNS-over-HTTPS so it runs in Node and the browser alike. Lower-level helpers (parseAtUri, nsidAuthority, resolveDid, resolveLexiconDid, getRecord, applyTemplate, …) are exported too.
Development
pnpm install
pnpm test # vitest (behavioural, fetch is mocked)
pnpm typecheck
pnpm build # tsup -> dual ESM/CJS + .d.ts in dist/