@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/README.md CHANGED
@@ -21,6 +21,16 @@ For a drop-in React picker UI, see [`@aturi.to/waypoints-react`](../waypoints-re
21
21
  npm install @aturi.to/waypoints
22
22
  ```
23
23
 
24
+ Also mirrored to GitHub Packages as `@atpota-to/waypoints` (GitHub only accepts a
25
+ scope matching the repository owner, and it rejects the dot in `aturi.to`). Same
26
+ build, same version. GitHub Packages requires a token even for public packages,
27
+ so installing from there needs an `.npmrc`:
28
+
29
+ ```sh
30
+ @atpota-to:registry=https://npm.pkg.github.com
31
+ //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # PAT with read:packages
32
+ ```
33
+
24
34
  ## Quick start
25
35
 
26
36
  ```ts
@@ -42,14 +52,140 @@ const fromUrl = await resolveUrl('https://bsky.app/profile/alice.bsky.social/pos
42
52
  - **Link builders & recommendations**: `getWaypointDataForType`,
43
53
  `getCategorizedWaypointsData`, `getRecommendedWaypointsData`,
44
54
  `getFeaturedWaypointData`, `waypointActivity`.
55
+ - **Compose intents**: `supportsComposeIntent`, `getComposeIntentUrl`,
56
+ `getComposeIntentAppUrl`, `getComposeIntentTemplate`,
57
+ `getComposeIntentWaypoints`, `describeComposeIntent`.
45
58
  - **AT URI parsing**: `parseURI`, `resolveHandle`, `getDisplayName`.
46
59
  - **Reverse resolution**: `matchSupportedUrl`, `parseAtUri`, `SUPPORTED_HOSTS`.
47
60
  - **High-level resolvers** (`resolve.ts`):
48
- - `buildWaypointsForParsed(parsed, { did?, excludeSourceId? })`
49
- - `resolveAtUri(uri)`
50
- - `resolveUrl(url, { fetchHead?, resolveHandle? })`
61
+ - `buildWaypointsForParsed(parsed, { did?, excludeSourceId?, composeText? })`
62
+ - `resolveAtUri(uri, { composeText? })`
63
+ - `resolveUrl(url, { fetchHead?, resolveHandle?, composeText? })`
51
64
  - `resolveViaApi(input, { endpoint? })`: typed client for the hosted
52
65
  `aturi.to/api/resolve` endpoint.
66
+ - **Universal links** (`universalLinks.ts`): `buildUniversalLink`,
67
+ `parseUniversalLink`, `isUniversalLink`, `describeUniversalLink`,
68
+ `buildUniversalLinkTags`, `UNIVERSAL_LINK_ORIGIN`.
69
+
70
+ ### Compose intents
71
+
72
+ bsky.app can be handed a link that opens its composer pre-filled —
73
+ `/intent/compose?text=…`, documented at
74
+ [docs.bsky.app](https://docs.bsky.app/docs/advanced-guides/intent-links) — and
75
+ the clients forked from the official social app inherit the same route. The
76
+ catalog records which ones do, so you can offer "post this in your client"
77
+ without hardcoding a list.
78
+
79
+ ```ts
80
+ import {
81
+ WAYPOINT_DESTINATIONS_DATA,
82
+ getComposeIntentUrl,
83
+ getComposeIntentWaypoints,
84
+ supportsComposeIntent,
85
+ } from '@aturi.to/waypoints';
86
+
87
+ getComposeIntentWaypoints().map((w) => w.id);
88
+ // ['anisota', 'bluesky', 'impro', 'blacksky', 'witchsky', 'mu', 'deer', 'northsky']
89
+
90
+ supportsComposeIntent(WAYPOINT_DESTINATIONS_DATA.pdsls); // false
91
+ getComposeIntentUrl(WAYPOINT_DESTINATIONS_DATA.deer, 'hello from my app');
92
+ // 'https://deer.social/intent/compose?text=hello%20from%20my%20app'
93
+ ```
94
+
95
+ Resolver results carry the same information per waypoint as a serializable
96
+ `composeIntent` (`null` when the client has no confirmed route), and
97
+ `resolveAtUri` / `resolveUrl` / `buildWaypointsForParsed` take a `composeText`
98
+ option to pre-fill it:
99
+
100
+ ```ts
101
+ const { waypoints } = resolveAtUri(uri, { composeText: 'look at this' })!;
102
+ waypoints.find((w) => w.id === 'deer')?.composeIntent;
103
+ // {
104
+ // url: 'https://deer.social/intent/compose?text=look%20at%20this',
105
+ // urlTemplate: 'https://deer.social/intent/compose?text={text}',
106
+ // textParam: 'text',
107
+ // prefillsText: true,
108
+ // }
109
+ ```
110
+
111
+ Two things not to assume. `prefillsText` is `false` for a client that routes
112
+ the intent but drops the text (Impro today), so the link opens an empty
113
+ composer — fine as a jump, useless as a share. And `appUrl` is only set where
114
+ the client publishes a native scheme (`bluesky://intent/compose`), so treat it
115
+ as a bonus rather than a fallback.
116
+
117
+ A missing `composeIntent` means "no route we've confirmed", not proof the
118
+ client lacks one. If a client you maintain handles compose intents,
119
+ [open an issue](https://github.com/atpota-to/aturi/issues) and we'll add it.
120
+
121
+ ### Universal links
122
+
123
+ A universal link is the client-agnostic address of a record: drop an
124
+ `aturi.to/…` URL in a DM or a footer and the recipient gets a preview plus every
125
+ client that can open it, instead of being pushed into whichever app you happen
126
+ to use. `buildUniversalLink` returns that address for anything that names a
127
+ record: an AT URI, a handle, a DID, a page URL from any client in the catalog,
128
+ a `ParsedURI`. It's pure, synchronous, and never fetches.
129
+
130
+ ```ts
131
+ import { buildUniversalLink, describeUniversalLink } from '@aturi.to/waypoints';
132
+
133
+ buildUniversalLink('at://did:plc:abc/app.bsky.feed.post/3k7');
134
+ // 'https://aturi.to/profile/did:plc:abc/post/3k7'
135
+ buildUniversalLink('https://bsky.app/profile/alice.bsky.social/post/3k7');
136
+ // 'https://aturi.to/profile/alice.bsky.social/post/3k7'
137
+ buildUniversalLink('@alice.bsky.social');
138
+ // 'https://aturi.to/profile/alice.bsky.social'
139
+ ```
140
+
141
+ For a copy button or a share sheet, `describeUniversalLink` returns the strings
142
+ around the link too:
143
+
144
+ ```ts
145
+ const link = describeUniversalLink('at://alice.bsky.social/app.bsky.feed.post/3k7');
146
+ link.url; // 'https://aturi.to/profile/alice.bsky.social/post/3k7'
147
+ link.label; // 'Post by @alice.bsky.social'
148
+ link.share; // { title, text, url }; hand it straight to navigator.share()
149
+ link.snippets.markdown; // '[Post by @alice.bsky.social](https://aturi.to/…)'
150
+ link.oembedUrl; // hosted oEmbed endpoint (posts only; null otherwise)
151
+ ```
152
+
153
+ Options on both: `origin` (point at your own deployment), `did` + `preferDid`
154
+ (address links by DID, which survives a handle change), and `params` for
155
+ appended query parameters like `{ ref: 'my-app' }`.
156
+
157
+ Going the other way, `parseUniversalLink` turns an aturi.to URL back into a
158
+ `ParsedURI`. Canonical `/profile/…` links, `/explore/…` views, and the legacy
159
+ bare-path and `at://`-in-path spellings all resolve:
160
+
161
+ ```ts
162
+ parseUniversalLink('https://aturi.to/profile/alice.bsky.social/post/3k7');
163
+ // { type: 'post', handle: 'alice.bsky.social', collection: 'app.bsky.feed.post', rkey: '3k7', … }
164
+ ```
165
+
166
+ #### Making your own pages resolvable
167
+
168
+ If your app renders atproto records, `buildUniversalLinkTags` writes the
169
+ `<head>` tags that let the rest of the Atmosphere find its way back to them:
170
+
171
+ ```ts
172
+ buildUniversalLinkTags('at://did:plc:abc/app.bsky.feed.post/3k7').html;
173
+ // <meta name="at:canonical" content="at://did:plc:abc/app.bsky.feed.post/3k7" />
174
+ // <meta name="at:author" content="at://did:plc:abc" />
175
+ // <link rel="alternate" href="at://did:plc:abc/app.bsky.feed.post/3k7" />
176
+ // <link rel="alternate" type="application/json+oembed" href="https://aturi.to/api/oembed?url=…" />
177
+ ```
178
+
179
+ `at:canonical` is the [AT Tags proposal](https://tangled.org/chrisshank.com/at-tags/).
180
+ Aturi's browser extension reads it off the live page and `/api/resolve` reads it
181
+ off your HTML, so a link to your page resolves into every other client that can
182
+ open the record, without your app being in the catalog at all. The
183
+ `<link rel="alternate" href="at://…">` beside it is the older spelling of the
184
+ same declaration, kept because the resolver still falls back to it. The oEmbed
185
+ pointer is emitted for posts only, since that's all the endpoint renders.
186
+
187
+ They're static strings describing a record you already display, and serving
188
+ them hands nothing to aturi.to.
53
189
 
54
190
  ### DID-only waypoints
55
191
 
@@ -65,6 +201,14 @@ flag and `resolveViaApi` let you fall back to fetching the page and probing for
65
201
  `resolveViaApi` is the right choice from a browser, where fetching arbitrary
66
202
  pages is blocked by CORS.
67
203
 
204
+ There's a second hosted endpoint for the catalog itself — what's in it, and
205
+ which clients can do what — for consumers that aren't installing the package:
206
+
207
+ ```http
208
+ GET https://aturi.to/api/waypoints
209
+ GET https://aturi.to/api/waypoints?type=post&capability=compose
210
+ ```
211
+
68
212
  ## A note on drift
69
213
 
70
214
  The four canonical logic/icon files (`waypoints.data.ts`, `uriParser.ts`,