type WaypointType = 'post' | 'profile' | 'list' | 'record' | 'unknown'; /** * Keys identifying which "data family" a waypoint belongs to for auto-redirect * purposes. Two waypoints can only be the endpoints of an auto-redirect when * they share at least one family key (i.e. they render the same underlying * atproto collections). Display groupings live in `WAYPOINT_CATEGORIES_DATA` * and are independent of this. */ type RedirectCompatFamily = 'bluesky-social' | 'standard-site' | 'tangled' | 'margin' | 'grain' | 'pinksky' | 'semble' | 'streamplace' | 'popfeed' | 'sifa' | 'blento' | 'atproto-explorer'; /** * A client's support for Bluesky-style *compose intent links*: a URL that opens * the app's composer, optionally pre-filled with text. * See https://docs.bsky.app/docs/advanced-guides/intent-links. * * bsky.app established `/intent/compose?text=…`, and the social-app forks in the * catalog inherit the same route, so a link built for one works on any of them. * Only add an entry once the client's own route has been confirmed — a link to * a client that doesn't handle it lands the user on a 404 or an empty home feed. */ type ComposeIntentData = { /** * The compose route, absolute and free of any query string * (e.g. `https://bsky.app/intent/compose`). */ url: string; /** * Query parameter carrying the pre-filled post text. Omitted when the client * routes the intent but ignores the text: the link still opens a composer, * just an empty one. Check this before offering a "share this to…" affordance, * since there the text is the whole point. * * Bluesky's post limit is 300 grapheme clusters; longer text is the caller's * problem to truncate. */ textParam?: string; /** * Deep link into the client's native app for the same intent, when it * publishes a scheme (e.g. `bluesky://intent/compose`). */ appUrl?: string; }; type WaypointData = { id: string; name: string; description: string | ((collection?: string, type?: WaypointType) => string); getUrl: (handle: string, collection?: string, rkey?: string, did?: string) => string | null; supportedTypes: WaypointType[]; category: string; /** * Compose intent support, when the client has a confirmed intent route. * Absent means "no known support" rather than a proven absence — read it with * `supportsComposeIntent` / `getComposeIntentUrl`. */ composeIntent?: ComposeIntentData; /** * Data families this waypoint participates in. Auto-redirect rules are only * emitted between waypoints that share at least one family. An empty array * means the waypoint can never be an auto-redirect source *or* destination * (dev tools / generic record viewers land here by design). */ redirectCompat: RedirectCompatFamily[]; /** * NSID prefixes that signal this waypoint is meaningfully usable for the * target repo. When set, the extension can call describeRepo on the target * DID and check whether any of the user's collections start with one of * these prefixes — if none do, the waypoint is flagged as "no records * found" in the popup and demoted in smart recommendations. * * Use trailing-dot prefixes for whole namespaces (e.g. `'sh.tangled.'`) * or full NSIDs for single-collection apps. Atmosphere apps typically * declare a reversed-domain prefix (`semble.so` → `so.semble.`) so the * popup can flag accounts that haven't published any of that app's * records. Omit the field entirely for generic explorers (PDSls, * atp.tools, Aturi) — those stay in the "unknown" / no-opinion state. */ expectedCollections?: string[]; }; type CompatFamilyMeta = { id: RedirectCompatFamily; name: string; description: string; }; /** * Registry of compat families. `description` is shown in the options UI so the * user understands what each "Favorite for X" controls. */ declare const COMPAT_FAMILIES: Record; declare const COMPAT_FAMILY_ORDER: RedirectCompatFamily[]; type WaypointCategoryData = { id: string; name: string; description?: string; defaultWaypointId: string; subcategories?: WaypointCategoryData[]; }; type CategorizedWaypointsData = { category: WaypointCategoryData; waypoints: WaypointData[]; }; declare const WAYPOINT_DESTINATIONS_DATA: Record; declare const WAYPOINT_ORDER: string[]; declare function getWaypointDataForType(type: WaypointType): WaypointData[]; declare function getWaypointCountData(): number; declare const WAYPOINT_CATEGORIES_DATA: Record; declare const CATEGORY_ORDER: string[]; declare function getCategorizedWaypointsData(type: WaypointType): CategorizedWaypointsData[]; declare function getRecommendedWaypointsData(type: WaypointType, collection?: string): { waypoints: WaypointData[]; label: string; }; declare function getFeaturedWaypointData(type: WaypointType, collection?: string): WaypointData | null; /** * Result of comparing a waypoint's `expectedCollections` against the set of * NSIDs found in the target repo. * * - 'present' — the user has at least one record under a matching prefix. * - 'absent' — the waypoint declared collections but none are in the repo. * - 'unknown' — the waypoint didn't declare any expectations, or we * haven't scanned the repo yet (e.g. scan disabled, no DID). */ type WaypointActivity = 'present' | 'absent' | 'unknown'; /** * Classify a waypoint against the set of collection NSIDs known to exist on * the target repo. Prefix-matches each entry in `expectedCollections` against * `repoCollections`. Returns 'unknown' when the waypoint has no declared * expectations or the caller passed `null` for `repoCollections` (i.e. scan * disabled or still in flight). */ declare function waypointActivity(waypoint: Pick, repoCollections: ReadonlySet | null): WaypointActivity; /** Placeholder a compose intent template leaves for the caller's post text. */ declare const COMPOSE_INTENT_TEXT_PLACEHOLDER = "{text}"; /** Whether the client can be handed a link that opens its composer. */ declare function supportsComposeIntent(waypoint: Pick): boolean; /** * Build a link that opens the client's composer, pre-filled with `text` when * the client reads it. Returns null when the client has no known intent route. * * getComposeIntentUrl(WAYPOINT_DESTINATIONS_DATA.deer, 'hello!') * // 'https://deer.social/intent/compose?text=hello!' */ declare function getComposeIntentUrl(waypoint: Pick, text?: string): string | null; /** * The native-app flavour of `getComposeIntentUrl`. Null unless the client * publishes a scheme of its own — most web clients don't, so fall back to the * https link rather than treating null as "unsupported". */ declare function getComposeIntentAppUrl(waypoint: Pick, text?: string): string | null; /** * The client's intent URL with a literal `{text}` where the post text goes, for * handing to consumers that build their own links (JSON APIs, docs, templates). * Substitute the placeholder with URL-encoded text. Clients that ignore the * text get a template with no placeholder at all. */ declare function getComposeIntentTemplate(waypoint: Pick): string | null; /** * Catalog-ordered list of every client with a compose intent route, optionally * narrowed to those that also render a given record type. */ declare function getComposeIntentWaypoints(type?: WaypointType): WaypointData[]; /** * JSON-safe view of a client's compose intent, for surfaces that can't ship a * function (HTTP responses, the extension's message passing, docs tables). */ type ComposeIntentDescriptor = { /** Ready to open. Pre-filled when `text` was supplied and the client reads it. */ url: string; /** The same URL with a literal `{text}` where the post text goes. */ urlTemplate: string; /** Query parameter carrying the text; null when the client ignores it. */ textParam: string | null; /** * False when the composer opens empty no matter what you pass — the link is * still a valid "start a post over there" jump, just not a share. */ prefillsText: boolean; /** Native-app deep link for the same intent, when the client publishes one. */ appUrl?: string; }; /** Serialize a waypoint's compose intent. Null when it has none. */ declare function describeComposeIntent(waypoint: Pick, text?: string): ComposeIntentDescriptor | null; type ParsedURI = { type: 'post' | 'profile' | 'list' | 'record' | 'unknown'; uri: string; handle: string; did?: string; collection?: string; rkey?: string; error?: string; }; /** * Parse URL path segments into structured AT URI data * Examples: * - /alice.bsky.social -> profile * - /alice.bsky.social/app.bsky.feed.post/3k7qw... -> post * - /did:plc:xxx/app.bsky.graph.list/abc -> list */ declare function parseURI(handle: string, collection?: string, rkey?: string): ParsedURI; /** * Resolve a handle to a DID using the Bluesky API */ /** * Handle resolution that distinguishes a definitive "no such handle" from a * transient "resolver unavailable". Callers that need to decide between a real * 404 and a retry state use this; `resolveHandle` remains for the common * did-or-null case. * * - `not-found`: the appview returned a 4xx (invalid/unknown handle). Safe to * surface as a 404. * - `unavailable`: network failure or 5xx. Must NOT be shown as a 404 — it's a * real account we couldn't look up right now. */ type HandleResolution = { did: string; reason?: undefined; } | { did: null; reason: 'not-found' | 'unavailable'; }; declare function resolveHandleStatus(handle: string): Promise; declare function resolveHandle(handle: string): Promise; /** * Get display name from handle or DID */ declare function getDisplayName(handle: string, did?: string): string; type SourceApp = 'aturi' | 'aturiExplore' | 'bluesky' | 'bluepy' | 'blacksky' | 'reddwarf' | 'impro' | 'lea' | 'witchsky' | 'deer' | 'northsky' | 'mu' | 'anisota' | 'pinksky' | 'leaflet' | 'tangled' | 'margin' | 'pdsls' | 'atptools' | 'semble' | 'streamplace' | 'grain' | 'popfeed' | 'sifa' | 'blento' | 'standardReader' | 'taproot' | 'offprint' | 'pckt' | 'headDetected'; type ReverseMatch = { source: SourceApp; parsed: ParsedURI; }; /** * Reverse-match any supported Aturi waypoint site URL back into a structured * ParsedURI (handle/collection/rkey). Returns `null` if the URL isn't on a * supported site or isn't a shape we recognize. */ declare function matchSupportedUrl(url: URL): ReverseMatch | null; /** * Parse an AT URI string (e.g. "at://did:plc:abc123/collection/rkey") into * its components. Used by head-based detection when an AT URI is found in * a tag's href attribute. */ declare function parseAtUri(uri: string): ReverseMatch | null; /** * All host names we know how to reverse-parse. Used by the popup + background * worker to decide if a tab is "relevant" before doing more expensive work. */ declare const SUPPORTED_HOSTS: string[]; /** * Whether a hostname belongs to a supported waypoint. Prefer this over a raw * `SUPPORTED_HOSTS.includes(...)` check: it strips a leading `www.` and also * recognizes subdomains of hosts that opt in (e.g. `eclose.anisota.net`), so * the extension popup and resolve API treat those tabs as known. */ declare function isSupportedHost(host: string): boolean; type ResolvedWaypoint = { id: string; name: string; category: string; url: string; /** * Whether this client can be handed a link that opens its composer, and how * to build one. Null when it has no confirmed compose intent route. Supply * `composeText` to get the links back pre-filled. */ composeIntent: ComposeIntentDescriptor | null; }; type ResolvedRecommendation = { ids: string[]; label: string; }; type ResolveResult = { parsed: ParsedURI; source: SourceApp; did: string | null; didResolved: boolean; waypoints: ResolvedWaypoint[]; recommended: ResolvedRecommendation; }; /** * Waypoints whose `getUrl` only produces a useful destination when a DID is * available. These are filtered out unless a DID is known. Mirrors the hosted * aturi.to/api/resolve route exactly. */ declare const DID_REQUIRED_WAYPOINTS: ReadonlySet; type BuildWaypointsOptions = { /** DID to pass to each waypoint's getUrl. Falls back to `parsed.did`. */ did?: string; /** Waypoint id to omit (e.g. the source app the user is already on). */ excludeSourceId?: string; /** Text to pre-fill into each waypoint's compose intent link, if it has one. */ composeText?: string; }; /** * Turn a parsed AT URI into the list of waypoints that can render it plus the * recommended set, applying the DID-required filter and dropping waypoints * whose getUrl returns null. This is the framework-agnostic core of the * hosted resolve endpoint. */ declare function buildWaypointsForParsed(parsed: ParsedURI, options?: BuildWaypointsOptions): { waypoints: ResolvedWaypoint[]; recommended: ResolvedRecommendation; }; /** * Resolve an AT URI string (e.g. "at://did:plc:abc/app.bsky.feed.post/rkey") * directly into its waypoints. Returns null if the string isn't a valid AT URI. */ declare function resolveAtUri(uri: string, options?: Pick): ResolveResult | null; type ResolveUrlOptions = { /** * When the URL pattern isn't recognized, fetch the page and look for a * `` in . Off by default to keep the resolver * isomorphic (no network unless explicitly requested). */ fetchHead?: boolean; /** Timeout for the optional head probe. Defaults to 4000ms. */ fetchHeadTimeoutMs?: number; /** * Resolve a handle to a DID so DID-only waypoints (pdsls, atptools, margin, * grain, popfeed) are included. Pass `resolveHandle` from this package, or * your own implementation. */ resolveHandle?: (handle: string) => Promise; /** Text to pre-fill into each waypoint's compose intent link, if it has one. */ composeText?: string; }; /** * Resolve a pasted/shared page URL back into the AT URI it represents and the * waypoints that can render it. Uses local URL-pattern matching by default; * optionally falls back to a `` link probe and/or handle→DID resolution. * * The source app's own waypoint is omitted from the result (you're already * there), mirroring the extension popup and hosted endpoint. */ declare function resolveUrl(url: string | URL, options?: ResolveUrlOptions): Promise; type ResolveApiInput = { url?: string; atUri?: string; /** Set false to skip the server-side probe. */ headDetect?: boolean; /** Text to pre-fill into the returned compose intent links. */ composeText?: string; }; type ResolveApiParsed = { type: WaypointType; uri: string; handle: string; did: string | null; collection: string | null; rkey: string | null; }; type ResolveApiSuccess = { ok: true; inputKind: 'atUri' | 'url'; /** * How the endpoint found the AT URI. `atTags` means the page declared it * itself via `` (or `at:alternate`); `headLink` is * the older `` the same probe falls back to. */ detectedVia: 'atUri' | 'urlPattern' | 'atTags' | 'headLink' | null; source: SourceApp; isKnownHost: boolean; parsed: ResolveApiParsed; didResolved: boolean; recommended: ResolvedRecommendation; waypoints: ResolvedWaypoint[]; }; type ResolveApiFailure = { ok: false; input?: string | null; inputKind?: 'atUri' | 'url'; isKnownHost?: boolean; reason?: string; message?: string; error?: string; }; type ResolveApiResponse = ResolveApiSuccess | ResolveApiFailure; type ResolveViaApiOptions = { /** Defaults to the hosted endpoint, https://aturi.to/api/resolve. */ endpoint?: string; /** Custom fetch implementation (e.g. a polyfill or instrumented client). */ fetch?: typeof fetch; signal?: AbortSignal; }; /** * Typed client for the hosted resolve endpoint. Use this when you want the * server to do the work — notably the link probe, which needs to fetch * the target page (something you may not want to do from the browser for CORS * reasons). Returns the same response shape the route emits. */ declare function resolveViaApi(input: ResolveApiInput, options?: ResolveViaApiOptions): Promise; /** * Universal links: the aturi.to URL for a record or an identity. * * A universal link is the client-agnostic address of an atproto record. Drop * one anywhere and the recipient lands on a preview of the record and picks * the client they want to open it in, instead of being pushed into whichever * app the sender happened to use. * * This module is the whole round trip: * - `buildUniversalLink` returns that address for anything that names a * record: an AT URI, a handle, a DID, a URL from any client in the catalog. * - `parseUniversalLink` turns one back into a `ParsedURI`. * - `describeUniversalLink` adds the strings a share sheet or a copy button * needs (a label, a `navigator.share()` payload, markdown/HTML snippets). * - `buildUniversalLinkTags` emits the `` tags that let *other* apps * be resolved back into records, the read side of the same trip. * * Everything here is pure and synchronous. Nothing fetches. */ /** Where universal links point unless an `origin` says otherwise. */ declare const UNIVERSAL_LINK_ORIGIN = "https://aturi.to"; /** The oEmbed link `type` attribute, per the spec's discovery section. */ declare const OEMBED_LINK_TYPE = "application/json+oembed"; /** * Anything that names a record or an identity: * - an AT URI (`at://did:plc:abc/app.bsky.feed.post/3k7`) * - a bare handle or DID (`alice.bsky.social`, `@alice.bsky.social`, `did:plc:abc`) * - a scheme-less AT URI (`alice.bsky.social/app.bsky.feed.post/3k7`) * - a page URL from any client in the catalog, an aturi.to link included * - a `ParsedURI` you already have from `parseURI` / `matchSupportedUrl` */ type UniversalLinkTarget = string | ParsedURI; type UniversalLinkOptions = { /** * Origin to build against. Defaults to aturi.to; point it at your own * deployment if you run a fork. Trailing slashes are trimmed. */ origin?: string; /** * DID for the target, for input that only carries a handle. Handle → DID * resolution is a network call, so this package never does it for you: * pass `resolveHandle`'s result if you want DID-stable links. */ did?: string; /** * Address every link by DID instead of handle. A handle can be reassigned * to another identity; a DID is stable for the life of the account, so a * DID link never rots. Ignored when no DID is known. */ preferDid?: boolean; /** Query parameters to append, e.g. `{ ref: 'my-app' }`. Empty values are dropped. */ params?: Record; }; /** `navigator.share()`'s payload, which most native share sheets also accept. */ type UniversalLinkSharePayload = { title: string; text: string; url: string; }; /** Ready-to-paste forms of the same link. */ type UniversalLinkSnippets = { url: string; atUri: string; markdown: string; html: string; }; type UniversalLink = { url: string; atUri: string; type: WaypointType; handle: string; did: string | null; collection: string | null; rkey: string | null; /** Human label for the target, e.g. `Post by @alice.bsky.social`. */ label: string; share: UniversalLinkSharePayload; snippets: UniversalLinkSnippets; /** oEmbed endpoint for this link, or null for anything that isn't a post. */ oembedUrl: string | null; }; type DescribeUniversalLinkOptions = UniversalLinkOptions & { /** Override the share sheet's title. Defaults to the label. */ title?: string; /** Override the share sheet's text. Defaults to the label. */ text?: string; }; type UniversalLinkMetaTag = { name: string; content: string; }; type UniversalLinkLinkTag = { rel: string; href: string; type?: string; }; type UniversalLinkTags = { meta: UniversalLinkMetaTag[]; link: UniversalLinkLinkTag[]; /** The same tags as a ready-to-paste `` fragment. */ html: string; }; /** * Build the aturi.to link for a target. Returns null for input that doesn't * name a record or an identity. * * ```ts * buildUniversalLink('at://did:plc:abc/app.bsky.feed.post/3k7'); * // 'https://aturi.to/profile/did:plc:abc/post/3k7' * buildUniversalLink('https://bsky.app/profile/alice.bsky.social/post/3k7'); * // 'https://aturi.to/profile/alice.bsky.social/post/3k7' * ``` */ declare function buildUniversalLink(input: UniversalLinkTarget, options?: UniversalLinkOptions): string | null; /** * Turn an aturi.to URL back into the record it addresses. Accepts every shape * the site serves: the canonical `/profile/…` links, the `/explore/…` record * views, the legacy bare-path (`aturi.to/{handle}/{collection}/{rkey}`) and * `at://`-in-path forms. Returns null for any other host or path. */ declare function parseUniversalLink(url: string | URL, options?: Pick): ParsedURI | null; /** Whether a URL is an aturi.to link this package can resolve to a record. */ declare function isUniversalLink(url: string | URL, options?: Pick): boolean; /** * Everything a copy button or a share sheet needs for one target: the link * itself, a human label, a `navigator.share()` payload, and the link in the * forms people paste it in. * * ```ts * const link = describeUniversalLink('at://did:plc:abc/app.bsky.feed.post/3k7'); * await navigator.share(link.share); * await navigator.clipboard.writeText(link.snippets.markdown); * ``` */ declare function describeUniversalLink(input: UniversalLinkTarget, options?: DescribeUniversalLinkOptions): UniversalLink | null; /** * `` tags that connect a page to the record it renders, so the rest of * the Atmosphere can find its way back. Two consumers today: * * - `` is the AT Tags proposal * (https://tangled.org/chrisshank.com/at-tags/). Aturi's browser extension * reads it off the live page and `aturi.to/api/resolve` reads it off the * HTML, which is what turns your URL into "…and here are the 25 other * clients that can open this". The `` * alongside it is the older spelling of the same declaration, kept because * the resolver still falls back to it. * - `` points unfurlers at the hosted * oEmbed endpoint, so a link to your page previews as the post it is. * Emitted for posts only, since that's all the endpoint renders. * * Serving these does not hand anything to aturi.to; they're static strings * describing a record you already display. */ declare function buildUniversalLinkTags(input: UniversalLinkTarget, options?: UniversalLinkOptions): UniversalLinkTags | null; export { type BuildWaypointsOptions, CATEGORY_ORDER, COMPAT_FAMILIES, COMPAT_FAMILY_ORDER, COMPOSE_INTENT_TEXT_PLACEHOLDER, type CategorizedWaypointsData, type CompatFamilyMeta, type ComposeIntentData, type ComposeIntentDescriptor, DID_REQUIRED_WAYPOINTS, type DescribeUniversalLinkOptions, type HandleResolution, OEMBED_LINK_TYPE, type ParsedURI, type RedirectCompatFamily, type ResolveApiFailure, type ResolveApiInput, type ResolveApiParsed, type ResolveApiResponse, type ResolveApiSuccess, type ResolveResult, type ResolveUrlOptions, type ResolveViaApiOptions, type ResolvedRecommendation, type ResolvedWaypoint, type ReverseMatch, SUPPORTED_HOSTS, type SourceApp, UNIVERSAL_LINK_ORIGIN, type UniversalLink, type UniversalLinkLinkTag, type UniversalLinkMetaTag, type UniversalLinkOptions, type UniversalLinkSharePayload, type UniversalLinkSnippets, type UniversalLinkTags, type UniversalLinkTarget, WAYPOINT_CATEGORIES_DATA, WAYPOINT_DESTINATIONS_DATA, WAYPOINT_ORDER, type WaypointActivity, type WaypointCategoryData, type WaypointData, type WaypointType, buildUniversalLink, buildUniversalLinkTags, buildWaypointsForParsed, describeComposeIntent, describeUniversalLink, getCategorizedWaypointsData, getComposeIntentAppUrl, getComposeIntentTemplate, getComposeIntentUrl, getComposeIntentWaypoints, getDisplayName, getFeaturedWaypointData, getRecommendedWaypointsData, getWaypointCountData, getWaypointDataForType, isSupportedHost, isUniversalLink, matchSupportedUrl, parseAtUri, parseURI, parseUniversalLink, resolveAtUri, resolveHandle, resolveHandleStatus, resolveUrl, resolveViaApi, supportsComposeIntent, waypointActivity };