@aturi.to/waypoints 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -6,7 +6,39 @@ type WaypointType = 'post' | 'profile' | 'list' | 'record' | 'unknown';
6
6
  * atproto collections). Display groupings live in `WAYPOINT_CATEGORIES_DATA`
7
7
  * and are independent of this.
8
8
  */
9
- type RedirectCompatFamily = 'bluesky-social' | 'standard-site' | 'tangled' | 'margin' | 'grain' | 'pinksky' | 'semble' | 'streamplace' | 'popfeed' | 'sifa' | 'blento';
9
+ type RedirectCompatFamily = 'bluesky-social' | 'standard-site' | 'tangled' | 'margin' | 'grain' | 'pinksky' | 'semble' | 'streamplace' | 'popfeed' | 'sifa' | 'blento' | 'atproto-explorer';
10
+ /**
11
+ * A client's support for Bluesky-style *compose intent links*: a URL that opens
12
+ * the app's composer, optionally pre-filled with text.
13
+ * See https://docs.bsky.app/docs/advanced-guides/intent-links.
14
+ *
15
+ * bsky.app established `/intent/compose?text=…`, and the social-app forks in the
16
+ * catalog inherit the same route, so a link built for one works on any of them.
17
+ * Only add an entry once the client's own route has been confirmed — a link to
18
+ * a client that doesn't handle it lands the user on a 404 or an empty home feed.
19
+ */
20
+ type ComposeIntentData = {
21
+ /**
22
+ * The compose route, absolute and free of any query string
23
+ * (e.g. `https://bsky.app/intent/compose`).
24
+ */
25
+ url: string;
26
+ /**
27
+ * Query parameter carrying the pre-filled post text. Omitted when the client
28
+ * routes the intent but ignores the text: the link still opens a composer,
29
+ * just an empty one. Check this before offering a "share this to…" affordance,
30
+ * since there the text is the whole point.
31
+ *
32
+ * Bluesky's post limit is 300 grapheme clusters; longer text is the caller's
33
+ * problem to truncate.
34
+ */
35
+ textParam?: string;
36
+ /**
37
+ * Deep link into the client's native app for the same intent, when it
38
+ * publishes a scheme (e.g. `bluesky://intent/compose`).
39
+ */
40
+ appUrl?: string;
41
+ };
10
42
  type WaypointData = {
11
43
  id: string;
12
44
  name: string;
@@ -14,6 +46,12 @@ type WaypointData = {
14
46
  getUrl: (handle: string, collection?: string, rkey?: string, did?: string) => string | null;
15
47
  supportedTypes: WaypointType[];
16
48
  category: string;
49
+ /**
50
+ * Compose intent support, when the client has a confirmed intent route.
51
+ * Absent means "no known support" rather than a proven absence — read it with
52
+ * `supportsComposeIntent` / `getComposeIntentUrl`.
53
+ */
54
+ composeIntent?: ComposeIntentData;
17
55
  /**
18
56
  * Data families this waypoint participates in. Auto-redirect rules are only
19
57
  * emitted between waypoints that share at least one family. An empty array
@@ -89,6 +127,57 @@ type WaypointActivity = 'present' | 'absent' | 'unknown';
89
127
  * disabled or still in flight).
90
128
  */
91
129
  declare function waypointActivity(waypoint: Pick<WaypointData, 'expectedCollections'>, repoCollections: ReadonlySet<string> | null): WaypointActivity;
130
+ /** Placeholder a compose intent template leaves for the caller's post text. */
131
+ declare const COMPOSE_INTENT_TEXT_PLACEHOLDER = "{text}";
132
+ /** Whether the client can be handed a link that opens its composer. */
133
+ declare function supportsComposeIntent(waypoint: Pick<WaypointData, 'composeIntent'>): boolean;
134
+ /**
135
+ * Build a link that opens the client's composer, pre-filled with `text` when
136
+ * the client reads it. Returns null when the client has no known intent route.
137
+ *
138
+ * getComposeIntentUrl(WAYPOINT_DESTINATIONS_DATA.deer, 'hello!')
139
+ * // 'https://deer.social/intent/compose?text=hello!'
140
+ */
141
+ declare function getComposeIntentUrl(waypoint: Pick<WaypointData, 'composeIntent'>, text?: string): string | null;
142
+ /**
143
+ * The native-app flavour of `getComposeIntentUrl`. Null unless the client
144
+ * publishes a scheme of its own — most web clients don't, so fall back to the
145
+ * https link rather than treating null as "unsupported".
146
+ */
147
+ declare function getComposeIntentAppUrl(waypoint: Pick<WaypointData, 'composeIntent'>, text?: string): string | null;
148
+ /**
149
+ * The client's intent URL with a literal `{text}` where the post text goes, for
150
+ * handing to consumers that build their own links (JSON APIs, docs, templates).
151
+ * Substitute the placeholder with URL-encoded text. Clients that ignore the
152
+ * text get a template with no placeholder at all.
153
+ */
154
+ declare function getComposeIntentTemplate(waypoint: Pick<WaypointData, 'composeIntent'>): string | null;
155
+ /**
156
+ * Catalog-ordered list of every client with a compose intent route, optionally
157
+ * narrowed to those that also render a given record type.
158
+ */
159
+ declare function getComposeIntentWaypoints(type?: WaypointType): WaypointData[];
160
+ /**
161
+ * JSON-safe view of a client's compose intent, for surfaces that can't ship a
162
+ * function (HTTP responses, the extension's message passing, docs tables).
163
+ */
164
+ type ComposeIntentDescriptor = {
165
+ /** Ready to open. Pre-filled when `text` was supplied and the client reads it. */
166
+ url: string;
167
+ /** The same URL with a literal `{text}` where the post text goes. */
168
+ urlTemplate: string;
169
+ /** Query parameter carrying the text; null when the client ignores it. */
170
+ textParam: string | null;
171
+ /**
172
+ * False when the composer opens empty no matter what you pass — the link is
173
+ * still a valid "start a post over there" jump, just not a share.
174
+ */
175
+ prefillsText: boolean;
176
+ /** Native-app deep link for the same intent, when the client publishes one. */
177
+ appUrl?: string;
178
+ };
179
+ /** Serialize a waypoint's compose intent. Null when it has none. */
180
+ declare function describeComposeIntent(waypoint: Pick<WaypointData, 'composeIntent'>, text?: string): ComposeIntentDescriptor | null;
92
181
 
93
182
  type ParsedURI = {
94
183
  type: 'post' | 'profile' | 'list' | 'record' | 'unknown';
@@ -110,13 +199,32 @@ declare function parseURI(handle: string, collection?: string, rkey?: string): P
110
199
  /**
111
200
  * Resolve a handle to a DID using the Bluesky API
112
201
  */
202
+ /**
203
+ * Handle resolution that distinguishes a definitive "no such handle" from a
204
+ * transient "resolver unavailable". Callers that need to decide between a real
205
+ * 404 and a retry state use this; `resolveHandle` remains for the common
206
+ * did-or-null case.
207
+ *
208
+ * - `not-found`: the appview returned a 4xx (invalid/unknown handle). Safe to
209
+ * surface as a 404.
210
+ * - `unavailable`: network failure or 5xx. Must NOT be shown as a 404 — it's a
211
+ * real account we couldn't look up right now.
212
+ */
213
+ type HandleResolution = {
214
+ did: string;
215
+ reason?: undefined;
216
+ } | {
217
+ did: null;
218
+ reason: 'not-found' | 'unavailable';
219
+ };
220
+ declare function resolveHandleStatus(handle: string): Promise<HandleResolution>;
113
221
  declare function resolveHandle(handle: string): Promise<string | null>;
114
222
  /**
115
223
  * Get display name from handle or DID
116
224
  */
117
225
  declare function getDisplayName(handle: string, did?: string): string;
118
226
 
119
- type SourceApp = 'bluesky' | 'bluepy' | 'blacksky' | 'reddwarf' | 'witchsky' | 'catsky' | 'deer' | 'anisota' | 'pinksky' | 'leaflet' | 'tangled' | 'margin' | 'pdsls' | 'atptools' | 'semble' | 'streamplace' | 'grain' | 'popfeed' | 'sifa' | 'blento' | 'offprint' | 'pckt' | 'headDetected';
227
+ 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';
120
228
  type ReverseMatch = {
121
229
  source: SourceApp;
122
230
  parsed: ParsedURI;
@@ -138,12 +246,25 @@ declare function parseAtUri(uri: string): ReverseMatch | null;
138
246
  * worker to decide if a tab is "relevant" before doing more expensive work.
139
247
  */
140
248
  declare const SUPPORTED_HOSTS: string[];
249
+ /**
250
+ * Whether a hostname belongs to a supported waypoint. Prefer this over a raw
251
+ * `SUPPORTED_HOSTS.includes(...)` check: it strips a leading `www.` and also
252
+ * recognizes subdomains of hosts that opt in (e.g. `eclose.anisota.net`), so
253
+ * the extension popup and resolve API treat those tabs as known.
254
+ */
255
+ declare function isSupportedHost(host: string): boolean;
141
256
 
142
257
  type ResolvedWaypoint = {
143
258
  id: string;
144
259
  name: string;
145
260
  category: string;
146
261
  url: string;
262
+ /**
263
+ * Whether this client can be handed a link that opens its composer, and how
264
+ * to build one. Null when it has no confirmed compose intent route. Supply
265
+ * `composeText` to get the links back pre-filled.
266
+ */
267
+ composeIntent: ComposeIntentDescriptor | null;
147
268
  };
148
269
  type ResolvedRecommendation = {
149
270
  ids: string[];
@@ -168,6 +289,8 @@ type BuildWaypointsOptions = {
168
289
  did?: string;
169
290
  /** Waypoint id to omit (e.g. the source app the user is already on). */
170
291
  excludeSourceId?: string;
292
+ /** Text to pre-fill into each waypoint's compose intent link, if it has one. */
293
+ composeText?: string;
171
294
  };
172
295
  /**
173
296
  * Turn a parsed AT URI into the list of waypoints that can render it plus the
@@ -183,7 +306,7 @@ declare function buildWaypointsForParsed(parsed: ParsedURI, options?: BuildWaypo
183
306
  * Resolve an AT URI string (e.g. "at://did:plc:abc/app.bsky.feed.post/rkey")
184
307
  * directly into its waypoints. Returns null if the string isn't a valid AT URI.
185
308
  */
186
- declare function resolveAtUri(uri: string): ResolveResult | null;
309
+ declare function resolveAtUri(uri: string, options?: Pick<BuildWaypointsOptions, 'composeText'>): ResolveResult | null;
187
310
  type ResolveUrlOptions = {
188
311
  /**
189
312
  * When the URL pattern isn't recognized, fetch the page and look for a
@@ -199,6 +322,8 @@ type ResolveUrlOptions = {
199
322
  * your own implementation.
200
323
  */
201
324
  resolveHandle?: (handle: string) => Promise<string | null>;
325
+ /** Text to pre-fill into each waypoint's compose intent link, if it has one. */
326
+ composeText?: string;
202
327
  };
203
328
  /**
204
329
  * Resolve a pasted/shared page URL back into the AT URI it represents and the
@@ -214,6 +339,8 @@ type ResolveApiInput = {
214
339
  atUri?: string;
215
340
  /** Set false to skip the server-side <head> probe. */
216
341
  headDetect?: boolean;
342
+ /** Text to pre-fill into the returned compose intent links. */
343
+ composeText?: string;
217
344
  };
218
345
  type ResolveApiParsed = {
219
346
  type: WaypointType;
@@ -226,7 +353,12 @@ type ResolveApiParsed = {
226
353
  type ResolveApiSuccess = {
227
354
  ok: true;
228
355
  inputKind: 'atUri' | 'url';
229
- detectedVia: 'atUri' | 'urlPattern' | 'headLink' | null;
356
+ /**
357
+ * How the endpoint found the AT URI. `atTags` means the page declared it
358
+ * itself via `<meta name="at:canonical">` (or `at:alternate`); `headLink` is
359
+ * the older `<link href="at://…">` the same probe falls back to.
360
+ */
361
+ detectedVia: 'atUri' | 'urlPattern' | 'atTags' | 'headLink' | null;
230
362
  source: SourceApp;
231
363
  isKnownHost: boolean;
232
364
  parsed: ResolveApiParsed;
@@ -259,4 +391,159 @@ type ResolveViaApiOptions = {
259
391
  */
260
392
  declare function resolveViaApi(input: ResolveApiInput, options?: ResolveViaApiOptions): Promise<ResolveApiResponse>;
261
393
 
262
- export { type BuildWaypointsOptions, CATEGORY_ORDER, COMPAT_FAMILIES, COMPAT_FAMILY_ORDER, type CategorizedWaypointsData, type CompatFamilyMeta, DID_REQUIRED_WAYPOINTS, 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, WAYPOINT_CATEGORIES_DATA, WAYPOINT_DESTINATIONS_DATA, WAYPOINT_ORDER, type WaypointActivity, type WaypointCategoryData, type WaypointData, type WaypointType, buildWaypointsForParsed, getCategorizedWaypointsData, getDisplayName, getFeaturedWaypointData, getRecommendedWaypointsData, getWaypointCountData, getWaypointDataForType, matchSupportedUrl, parseAtUri, parseURI, resolveAtUri, resolveHandle, resolveUrl, resolveViaApi, waypointActivity };
394
+ /**
395
+ * Universal links: the aturi.to URL for a record or an identity.
396
+ *
397
+ * A universal link is the client-agnostic address of an atproto record. Drop
398
+ * one anywhere and the recipient lands on a preview of the record and picks
399
+ * the client they want to open it in, instead of being pushed into whichever
400
+ * app the sender happened to use.
401
+ *
402
+ * This module is the whole round trip:
403
+ * - `buildUniversalLink` returns that address for anything that names a
404
+ * record: an AT URI, a handle, a DID, a URL from any client in the catalog.
405
+ * - `parseUniversalLink` turns one back into a `ParsedURI`.
406
+ * - `describeUniversalLink` adds the strings a share sheet or a copy button
407
+ * needs (a label, a `navigator.share()` payload, markdown/HTML snippets).
408
+ * - `buildUniversalLinkTags` emits the `<head>` tags that let *other* apps
409
+ * be resolved back into records, the read side of the same trip.
410
+ *
411
+ * Everything here is pure and synchronous. Nothing fetches.
412
+ */
413
+ /** Where universal links point unless an `origin` says otherwise. */
414
+ declare const UNIVERSAL_LINK_ORIGIN = "https://aturi.to";
415
+ /** The oEmbed link `type` attribute, per the spec's discovery section. */
416
+ declare const OEMBED_LINK_TYPE = "application/json+oembed";
417
+ /**
418
+ * Anything that names a record or an identity:
419
+ * - an AT URI (`at://did:plc:abc/app.bsky.feed.post/3k7`)
420
+ * - a bare handle or DID (`alice.bsky.social`, `@alice.bsky.social`, `did:plc:abc`)
421
+ * - a scheme-less AT URI (`alice.bsky.social/app.bsky.feed.post/3k7`)
422
+ * - a page URL from any client in the catalog, an aturi.to link included
423
+ * - a `ParsedURI` you already have from `parseURI` / `matchSupportedUrl`
424
+ */
425
+ type UniversalLinkTarget = string | ParsedURI;
426
+ type UniversalLinkOptions = {
427
+ /**
428
+ * Origin to build against. Defaults to aturi.to; point it at your own
429
+ * deployment if you run a fork. Trailing slashes are trimmed.
430
+ */
431
+ origin?: string;
432
+ /**
433
+ * DID for the target, for input that only carries a handle. Handle → DID
434
+ * resolution is a network call, so this package never does it for you:
435
+ * pass `resolveHandle`'s result if you want DID-stable links.
436
+ */
437
+ did?: string;
438
+ /**
439
+ * Address every link by DID instead of handle. A handle can be reassigned
440
+ * to another identity; a DID is stable for the life of the account, so a
441
+ * DID link never rots. Ignored when no DID is known.
442
+ */
443
+ preferDid?: boolean;
444
+ /** Query parameters to append, e.g. `{ ref: 'my-app' }`. Empty values are dropped. */
445
+ params?: Record<string, string | number | boolean | null | undefined>;
446
+ };
447
+ /** `navigator.share()`'s payload, which most native share sheets also accept. */
448
+ type UniversalLinkSharePayload = {
449
+ title: string;
450
+ text: string;
451
+ url: string;
452
+ };
453
+ /** Ready-to-paste forms of the same link. */
454
+ type UniversalLinkSnippets = {
455
+ url: string;
456
+ atUri: string;
457
+ markdown: string;
458
+ html: string;
459
+ };
460
+ type UniversalLink = {
461
+ url: string;
462
+ atUri: string;
463
+ type: WaypointType;
464
+ handle: string;
465
+ did: string | null;
466
+ collection: string | null;
467
+ rkey: string | null;
468
+ /** Human label for the target, e.g. `Post by @alice.bsky.social`. */
469
+ label: string;
470
+ share: UniversalLinkSharePayload;
471
+ snippets: UniversalLinkSnippets;
472
+ /** oEmbed endpoint for this link, or null for anything that isn't a post. */
473
+ oembedUrl: string | null;
474
+ };
475
+ type DescribeUniversalLinkOptions = UniversalLinkOptions & {
476
+ /** Override the share sheet's title. Defaults to the label. */
477
+ title?: string;
478
+ /** Override the share sheet's text. Defaults to the label. */
479
+ text?: string;
480
+ };
481
+ type UniversalLinkMetaTag = {
482
+ name: string;
483
+ content: string;
484
+ };
485
+ type UniversalLinkLinkTag = {
486
+ rel: string;
487
+ href: string;
488
+ type?: string;
489
+ };
490
+ type UniversalLinkTags = {
491
+ meta: UniversalLinkMetaTag[];
492
+ link: UniversalLinkLinkTag[];
493
+ /** The same tags as a ready-to-paste `<head>` fragment. */
494
+ html: string;
495
+ };
496
+ /**
497
+ * Build the aturi.to link for a target. Returns null for input that doesn't
498
+ * name a record or an identity.
499
+ *
500
+ * ```ts
501
+ * buildUniversalLink('at://did:plc:abc/app.bsky.feed.post/3k7');
502
+ * // 'https://aturi.to/profile/did:plc:abc/post/3k7'
503
+ * buildUniversalLink('https://bsky.app/profile/alice.bsky.social/post/3k7');
504
+ * // 'https://aturi.to/profile/alice.bsky.social/post/3k7'
505
+ * ```
506
+ */
507
+ declare function buildUniversalLink(input: UniversalLinkTarget, options?: UniversalLinkOptions): string | null;
508
+ /**
509
+ * Turn an aturi.to URL back into the record it addresses. Accepts every shape
510
+ * the site serves: the canonical `/profile/…` links, the `/explore/…` record
511
+ * views, the legacy bare-path (`aturi.to/{handle}/{collection}/{rkey}`) and
512
+ * `at://`-in-path forms. Returns null for any other host or path.
513
+ */
514
+ declare function parseUniversalLink(url: string | URL, options?: Pick<UniversalLinkOptions, 'origin'>): ParsedURI | null;
515
+ /** Whether a URL is an aturi.to link this package can resolve to a record. */
516
+ declare function isUniversalLink(url: string | URL, options?: Pick<UniversalLinkOptions, 'origin'>): boolean;
517
+ /**
518
+ * Everything a copy button or a share sheet needs for one target: the link
519
+ * itself, a human label, a `navigator.share()` payload, and the link in the
520
+ * forms people paste it in.
521
+ *
522
+ * ```ts
523
+ * const link = describeUniversalLink('at://did:plc:abc/app.bsky.feed.post/3k7');
524
+ * await navigator.share(link.share);
525
+ * await navigator.clipboard.writeText(link.snippets.markdown);
526
+ * ```
527
+ */
528
+ declare function describeUniversalLink(input: UniversalLinkTarget, options?: DescribeUniversalLinkOptions): UniversalLink | null;
529
+ /**
530
+ * `<head>` tags that connect a page to the record it renders, so the rest of
531
+ * the Atmosphere can find its way back. Two consumers today:
532
+ *
533
+ * - `<meta name="at:canonical">` is the AT Tags proposal
534
+ * (https://tangled.org/chrisshank.com/at-tags/). Aturi's browser extension
535
+ * reads it off the live page and `aturi.to/api/resolve` reads it off the
536
+ * HTML, which is what turns your URL into "…and here are the 25 other
537
+ * clients that can open this". The `<link rel="alternate" href="at://…">`
538
+ * alongside it is the older spelling of the same declaration, kept because
539
+ * the resolver still falls back to it.
540
+ * - `<link type="application/json+oembed">` points unfurlers at the hosted
541
+ * oEmbed endpoint, so a link to your page previews as the post it is.
542
+ * Emitted for posts only, since that's all the endpoint renders.
543
+ *
544
+ * Serving these does not hand anything to aturi.to; they're static strings
545
+ * describing a record you already display.
546
+ */
547
+ declare function buildUniversalLinkTags(input: UniversalLinkTarget, options?: UniversalLinkOptions): UniversalLinkTags | null;
548
+
549
+ 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 };