@aturi.to/waypoints 0.1.2 → 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
@@ -7,6 +7,38 @@ type WaypointType = 'post' | 'profile' | 'list' | 'record' | 'unknown';
7
7
  * and are independent of this.
8
8
  */
9
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';
@@ -135,7 +224,7 @@ declare function resolveHandle(handle: string): Promise<string | null>;
135
224
  */
136
225
  declare function getDisplayName(handle: string, did?: string): string;
137
226
 
138
- type SourceApp = 'aturi' | 'aturiExplore' | 'bluesky' | 'bluepy' | 'blacksky' | 'reddwarf' | 'impro' | 'witchsky' | 'deer' | 'mu' | 'anisota' | 'pinksky' | 'leaflet' | 'tangled' | 'margin' | 'pdsls' | 'atptools' | 'semble' | 'streamplace' | 'grain' | 'popfeed' | 'sifa' | 'blento' | 'standardReader' | 'taproot' | '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';
139
228
  type ReverseMatch = {
140
229
  source: SourceApp;
141
230
  parsed: ParsedURI;
@@ -170,6 +259,12 @@ type ResolvedWaypoint = {
170
259
  name: string;
171
260
  category: string;
172
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;
173
268
  };
174
269
  type ResolvedRecommendation = {
175
270
  ids: string[];
@@ -194,6 +289,8 @@ type BuildWaypointsOptions = {
194
289
  did?: string;
195
290
  /** Waypoint id to omit (e.g. the source app the user is already on). */
196
291
  excludeSourceId?: string;
292
+ /** Text to pre-fill into each waypoint's compose intent link, if it has one. */
293
+ composeText?: string;
197
294
  };
198
295
  /**
199
296
  * Turn a parsed AT URI into the list of waypoints that can render it plus the
@@ -209,7 +306,7 @@ declare function buildWaypointsForParsed(parsed: ParsedURI, options?: BuildWaypo
209
306
  * Resolve an AT URI string (e.g. "at://did:plc:abc/app.bsky.feed.post/rkey")
210
307
  * directly into its waypoints. Returns null if the string isn't a valid AT URI.
211
308
  */
212
- declare function resolveAtUri(uri: string): ResolveResult | null;
309
+ declare function resolveAtUri(uri: string, options?: Pick<BuildWaypointsOptions, 'composeText'>): ResolveResult | null;
213
310
  type ResolveUrlOptions = {
214
311
  /**
215
312
  * When the URL pattern isn't recognized, fetch the page and look for a
@@ -225,6 +322,8 @@ type ResolveUrlOptions = {
225
322
  * your own implementation.
226
323
  */
227
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;
228
327
  };
229
328
  /**
230
329
  * Resolve a pasted/shared page URL back into the AT URI it represents and the
@@ -240,6 +339,8 @@ type ResolveApiInput = {
240
339
  atUri?: string;
241
340
  /** Set false to skip the server-side <head> probe. */
242
341
  headDetect?: boolean;
342
+ /** Text to pre-fill into the returned compose intent links. */
343
+ composeText?: string;
243
344
  };
244
345
  type ResolveApiParsed = {
245
346
  type: WaypointType;
@@ -252,7 +353,12 @@ type ResolveApiParsed = {
252
353
  type ResolveApiSuccess = {
253
354
  ok: true;
254
355
  inputKind: 'atUri' | 'url';
255
- 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;
256
362
  source: SourceApp;
257
363
  isKnownHost: boolean;
258
364
  parsed: ResolveApiParsed;
@@ -285,4 +391,159 @@ type ResolveViaApiOptions = {
285
391
  */
286
392
  declare function resolveViaApi(input: ResolveApiInput, options?: ResolveViaApiOptions): Promise<ResolveApiResponse>;
287
393
 
288
- export { type BuildWaypointsOptions, CATEGORY_ORDER, COMPAT_FAMILIES, COMPAT_FAMILY_ORDER, type CategorizedWaypointsData, type CompatFamilyMeta, DID_REQUIRED_WAYPOINTS, type HandleResolution, 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, isSupportedHost, matchSupportedUrl, parseAtUri, parseURI, resolveAtUri, resolveHandle, resolveHandleStatus, 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 };