@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.js CHANGED
@@ -75,6 +75,9 @@ var COMPAT_FAMILY_ORDER = [
75
75
  "blento",
76
76
  "atproto-explorer"
77
77
  ];
78
+ function socialAppComposeIntent(origin) {
79
+ return { url: `${origin}/intent/compose`, textParam: "text" };
80
+ }
78
81
  var WAYPOINT_DESTINATIONS_DATA = {
79
82
  aturi: {
80
83
  id: "aturi",
@@ -134,6 +137,11 @@ var WAYPOINT_DESTINATIONS_DATA = {
134
137
  },
135
138
  supportedTypes: ["post", "profile", "list", "record"],
136
139
  category: "blueskyClients",
140
+ // Anisota implements the Bluesky shape deliberately, then extends it with
141
+ // `url`, `tags`, `channel`, and `title` (and answers `/post` with the same
142
+ // query). What's declared here is the interoperable subset every caller can
143
+ // rely on; the extras are Anisota-only and unrepresentable in this field.
144
+ composeIntent: socialAppComposeIntent("https://anisota.net"),
137
145
  redirectCompat: ["bluesky-social", "standard-site"],
138
146
  expectedCollections: ["app.bsky.", "net.anisota."]
139
147
  },
@@ -150,12 +158,19 @@ var WAYPOINT_DESTINATIONS_DATA = {
150
158
  if (collection === "app.bsky.feed.post") {
151
159
  return `https://bsky.app/profile/${handle}/post/${rkey}`;
152
160
  }
161
+ if (collection === "app.bsky.graph.list") {
162
+ return `https://bsky.app/profile/${handle}/lists/${rkey}`;
163
+ }
153
164
  return `https://bsky.app/profile/${handle}`;
154
165
  }
155
166
  return `https://bsky.app/profile/${handle}`;
156
167
  },
157
168
  supportedTypes: ["post", "profile", "list", "record"],
158
169
  category: "blueskyClients",
170
+ composeIntent: {
171
+ ...socialAppComposeIntent("https://bsky.app"),
172
+ appUrl: "bluesky://intent/compose"
173
+ },
159
174
  redirectCompat: ["bluesky-social"],
160
175
  expectedCollections: ["app.bsky."]
161
176
  },
@@ -172,12 +187,16 @@ var WAYPOINT_DESTINATIONS_DATA = {
172
187
  if (collection === "app.bsky.feed.post") {
173
188
  return `https://blacksky.community/profile/${handle}/post/${rkey}`;
174
189
  }
190
+ if (collection === "app.bsky.graph.list") {
191
+ return `https://blacksky.community/profile/${handle}/lists/${rkey}`;
192
+ }
175
193
  return `https://blacksky.community/profile/${handle}`;
176
194
  }
177
195
  return `https://blacksky.community/profile/${handle}`;
178
196
  },
179
197
  supportedTypes: ["post", "profile", "list", "record"],
180
198
  category: "blueskyForks",
199
+ composeIntent: socialAppComposeIntent("https://blacksky.community"),
181
200
  redirectCompat: ["bluesky-social"],
182
201
  expectedCollections: ["app.bsky."]
183
202
  },
@@ -186,7 +205,6 @@ var WAYPOINT_DESTINATIONS_DATA = {
186
205
  name: "Red Dwarf",
187
206
  description: (collection) => {
188
207
  if (collection === "app.bsky.feed.post") return "View post on reddwarf.app";
189
- if (collection === "app.bsky.graph.list") return "View list on reddwarf.app";
190
208
  return "View profile on reddwarf.app";
191
209
  },
192
210
  getUrl: (handle, collection, rkey) => {
@@ -198,7 +216,10 @@ var WAYPOINT_DESTINATIONS_DATA = {
198
216
  }
199
217
  return `https://reddwarf.app/profile/${handle}`;
200
218
  },
201
- supportedTypes: ["post", "profile", "list", "record"],
219
+ // No `list`: unlike the social-app forks in this family, Red Dwarf has no
220
+ // `/profile/:handle/lists/:rkey` route, so offering it for a list would
221
+ // silently land the user on the list's author instead.
222
+ supportedTypes: ["post", "profile", "record"],
202
223
  category: "blueskyClients",
203
224
  redirectCompat: ["bluesky-social"],
204
225
  expectedCollections: ["app.bsky."]
@@ -225,6 +246,33 @@ var WAYPOINT_DESTINATIONS_DATA = {
225
246
  },
226
247
  supportedTypes: ["post", "profile", "list", "record"],
227
248
  category: "blueskyClients",
249
+ // Impro routes /intent/compose to its home view, which opens the composer
250
+ // for a signed-in user — but it never reads `?text`, so no textParam here.
251
+ composeIntent: { url: "https://impro.social/intent/compose" },
252
+ redirectCompat: ["bluesky-social"],
253
+ expectedCollections: ["app.bsky."]
254
+ },
255
+ lea: {
256
+ id: "lea",
257
+ name: "Lea",
258
+ description: (collection) => {
259
+ if (collection === "app.bsky.feed.post") return "View post on lea.ac";
260
+ return "View profile on lea.ac";
261
+ },
262
+ getUrl: (handle, collection, rkey) => {
263
+ if (collection && rkey) {
264
+ if (collection === "app.bsky.feed.post") {
265
+ return `https://lea.ac/profile/${handle}/post/${rkey}`;
266
+ }
267
+ return `https://lea.ac/profile/${handle}`;
268
+ }
269
+ return `https://lea.ac/profile/${handle}`;
270
+ },
271
+ // No `list`: like Red Dwarf, Lea serves only `/profile/:handle` and
272
+ // `/profile/:handle/post/:rkey` — `/lists/:rkey` 404s — so claiming list
273
+ // support would drop the user on an error page instead of the list.
274
+ supportedTypes: ["post", "profile", "record"],
275
+ category: "blueskyClients",
228
276
  redirectCompat: ["bluesky-social"],
229
277
  expectedCollections: ["app.bsky."]
230
278
  },
@@ -325,12 +373,16 @@ var WAYPOINT_DESTINATIONS_DATA = {
325
373
  if (collection === "app.bsky.feed.post") {
326
374
  return `https://witchsky.app/profile/${handle}/post/${rkey}`;
327
375
  }
376
+ if (collection === "app.bsky.graph.list") {
377
+ return `https://witchsky.app/profile/${handle}/lists/${rkey}`;
378
+ }
328
379
  return `https://witchsky.app/profile/${handle}`;
329
380
  }
330
381
  return `https://witchsky.app/profile/${handle}`;
331
382
  },
332
383
  supportedTypes: ["post", "profile", "list", "record"],
333
384
  category: "blueskyForks",
385
+ composeIntent: socialAppComposeIntent("https://witchsky.app"),
334
386
  redirectCompat: ["bluesky-social"],
335
387
  expectedCollections: ["app.bsky."]
336
388
  },
@@ -347,12 +399,16 @@ var WAYPOINT_DESTINATIONS_DATA = {
347
399
  if (collection === "app.bsky.feed.post") {
348
400
  return `https://deer.social/profile/${handle}/post/${rkey}`;
349
401
  }
402
+ if (collection === "app.bsky.graph.list") {
403
+ return `https://deer.social/profile/${handle}/lists/${rkey}`;
404
+ }
350
405
  return `https://deer.social/profile/${handle}`;
351
406
  }
352
407
  return `https://deer.social/profile/${handle}`;
353
408
  },
354
409
  supportedTypes: ["post", "profile", "list", "record"],
355
410
  category: "blueskyForks",
411
+ composeIntent: socialAppComposeIntent("https://deer.social"),
356
412
  redirectCompat: ["bluesky-social"],
357
413
  expectedCollections: ["app.bsky."]
358
414
  },
@@ -369,12 +425,42 @@ var WAYPOINT_DESTINATIONS_DATA = {
369
425
  if (collection === "app.bsky.feed.post") {
370
426
  return `https://mu.social/profile/${handle}/post/${rkey}`;
371
427
  }
428
+ if (collection === "app.bsky.graph.list") {
429
+ return `https://mu.social/profile/${handle}/lists/${rkey}`;
430
+ }
372
431
  return `https://mu.social/profile/${handle}`;
373
432
  }
374
433
  return `https://mu.social/profile/${handle}`;
375
434
  },
376
435
  supportedTypes: ["post", "profile", "list", "record"],
377
436
  category: "blueskyForks",
437
+ composeIntent: socialAppComposeIntent("https://mu.social"),
438
+ redirectCompat: ["bluesky-social"],
439
+ expectedCollections: ["app.bsky."]
440
+ },
441
+ northsky: {
442
+ id: "northsky",
443
+ name: "Northsky",
444
+ description: (collection) => {
445
+ if (collection === "app.bsky.feed.post") return "View post on northsky.app";
446
+ if (collection === "app.bsky.graph.list") return "View list on northsky.app";
447
+ return "View profile on northsky.app";
448
+ },
449
+ getUrl: (handle, collection, rkey) => {
450
+ if (collection && rkey) {
451
+ if (collection === "app.bsky.feed.post") {
452
+ return `https://northsky.app/profile/${handle}/post/${rkey}`;
453
+ }
454
+ if (collection === "app.bsky.graph.list") {
455
+ return `https://northsky.app/profile/${handle}/lists/${rkey}`;
456
+ }
457
+ return `https://northsky.app/profile/${handle}`;
458
+ }
459
+ return `https://northsky.app/profile/${handle}`;
460
+ },
461
+ supportedTypes: ["post", "profile", "list", "record"],
462
+ category: "blueskyForks",
463
+ composeIntent: socialAppComposeIntent("https://northsky.app"),
378
464
  redirectCompat: ["bluesky-social"],
379
465
  expectedCollections: ["app.bsky."]
380
466
  },
@@ -653,7 +739,9 @@ var WAYPOINT_ORDER = [
653
739
  "taproot",
654
740
  "witchsky",
655
741
  "mu",
656
- "deer"
742
+ "deer",
743
+ "lea",
744
+ "northsky"
657
745
  ];
658
746
  function getWaypointDataForType(type) {
659
747
  return WAYPOINT_ORDER.map((id) => WAYPOINT_DESTINATIONS_DATA[id]).filter((waypoint) => waypoint.supportedTypes.includes(type));
@@ -820,6 +908,47 @@ function waypointActivity(waypoint, repoCollections) {
820
908
  }
821
909
  return "absent";
822
910
  }
911
+ var COMPOSE_INTENT_TEXT_PLACEHOLDER = "{text}";
912
+ function supportsComposeIntent(waypoint) {
913
+ return !!waypoint.composeIntent;
914
+ }
915
+ function getComposeIntentUrl(waypoint, text) {
916
+ const intent = waypoint.composeIntent;
917
+ if (!intent) return null;
918
+ return appendComposeText(intent.url, intent.textParam, text);
919
+ }
920
+ function getComposeIntentAppUrl(waypoint, text) {
921
+ const intent = waypoint.composeIntent;
922
+ if (!intent?.appUrl) return null;
923
+ return appendComposeText(intent.appUrl, intent.textParam, text);
924
+ }
925
+ function getComposeIntentTemplate(waypoint) {
926
+ const intent = waypoint.composeIntent;
927
+ if (!intent) return null;
928
+ if (!intent.textParam) return intent.url;
929
+ return `${intent.url}?${intent.textParam}=${COMPOSE_INTENT_TEXT_PLACEHOLDER}`;
930
+ }
931
+ function getComposeIntentWaypoints(type) {
932
+ const waypoints = type ? getWaypointDataForType(type) : WAYPOINT_ORDER.map((id) => WAYPOINT_DESTINATIONS_DATA[id]).filter(Boolean);
933
+ return waypoints.filter(supportsComposeIntent);
934
+ }
935
+ function describeComposeIntent(waypoint, text) {
936
+ const intent = waypoint.composeIntent;
937
+ if (!intent) return null;
938
+ const descriptor = {
939
+ url: getComposeIntentUrl(waypoint, text),
940
+ urlTemplate: getComposeIntentTemplate(waypoint),
941
+ textParam: intent.textParam ?? null,
942
+ prefillsText: !!intent.textParam
943
+ };
944
+ const appUrl = getComposeIntentAppUrl(waypoint, text);
945
+ if (appUrl) descriptor.appUrl = appUrl;
946
+ return descriptor;
947
+ }
948
+ function appendComposeText(base, textParam, text) {
949
+ if (!textParam || !text) return base;
950
+ return `${base}?${textParam}=${encodeURIComponent(text)}`;
951
+ }
823
952
 
824
953
  // src/upstreamFetch.ts
825
954
  var RETRY_DELAY_MS = 250;
@@ -918,8 +1047,10 @@ var BLUESKY_FAMILY = [
918
1047
  { source: "blacksky", hosts: ["blacksky.community"] },
919
1048
  { source: "reddwarf", hosts: ["reddwarf.app"] },
920
1049
  { source: "impro", hosts: ["impro.social"] },
1050
+ { source: "lea", hosts: ["lea.ac"] },
921
1051
  { source: "witchsky", hosts: ["witchsky.app"] },
922
1052
  { source: "deer", hosts: ["deer.social"] },
1053
+ { source: "northsky", hosts: ["northsky.app"] },
923
1054
  { source: "mu", hosts: ["mu.social"] },
924
1055
  { source: "anisota", hosts: ["anisota.net"], matchSubdomains: true }
925
1056
  ];
@@ -1339,7 +1470,7 @@ function matchTaproot(host, pathname) {
1339
1470
  }
1340
1471
  function matchAturi(host, parts) {
1341
1472
  if (host !== "aturi.to") return null;
1342
- const isIdentifier = (seg) => !!seg && (seg.startsWith("did:") || seg.includes("."));
1473
+ const isIdentifier = (seg2) => !!seg2 && (seg2.startsWith("did:") || seg2.includes("."));
1343
1474
  if (parts[0] === "explore" && isIdentifier(parts[1])) {
1344
1475
  const handle = parts[1];
1345
1476
  const did = handle.startsWith("did:") ? handle : void 0;
@@ -1488,18 +1619,26 @@ function buildWaypointsForParsed(parsed, options = {}) {
1488
1619
  if (DID_REQUIRED_WAYPOINTS.has(w.id) && !did) return null;
1489
1620
  const url = w.getUrl(parsed.handle, parsed.collection, parsed.rkey, did);
1490
1621
  if (!url) return null;
1491
- return { id: w.id, name: w.name, category: w.category, url };
1622
+ return {
1623
+ id: w.id,
1624
+ name: w.name,
1625
+ category: w.category,
1626
+ url,
1627
+ composeIntent: describeComposeIntent(w, options.composeText)
1628
+ };
1492
1629
  }).filter((w) => !!w);
1493
1630
  const recommendedRaw = getRecommendedWaypointsData(type, parsed.collection);
1494
1631
  const availableIds = new Set(waypoints.map((w) => w.id));
1495
1632
  const ids = recommendedRaw.waypoints.map((w) => w.id).filter((id) => id !== exclude && availableIds.has(id));
1496
1633
  return { waypoints, recommended: { ids, label: recommendedRaw.label } };
1497
1634
  }
1498
- function resolveAtUri(uri) {
1635
+ function resolveAtUri(uri, options = {}) {
1499
1636
  const match = parseAtUri(uri);
1500
1637
  if (!match) return null;
1501
1638
  const { parsed, source } = match;
1502
- const { waypoints, recommended } = buildWaypointsForParsed(parsed);
1639
+ const { waypoints, recommended } = buildWaypointsForParsed(parsed, {
1640
+ composeText: options.composeText
1641
+ });
1503
1642
  return {
1504
1643
  parsed,
1505
1644
  source,
@@ -1541,7 +1680,8 @@ async function resolveUrl(url, options = {}) {
1541
1680
  }
1542
1681
  const { waypoints, recommended } = buildWaypointsForParsed(parsed, {
1543
1682
  did,
1544
- excludeSourceId: source
1683
+ excludeSourceId: source,
1684
+ composeText: options.composeText
1545
1685
  });
1546
1686
  return {
1547
1687
  parsed: { ...parsed, did },
@@ -1564,6 +1704,7 @@ async function resolveViaApi(input, options = {}) {
1564
1704
  throw new Error("resolveViaApi requires either `url` or `atUri`");
1565
1705
  }
1566
1706
  if (input.headDetect === false) params.set("headDetect", "false");
1707
+ if (input.composeText) params.set("composeText", input.composeText);
1567
1708
  const res = await fetchImpl(`${endpoint}?${params.toString()}`, {
1568
1709
  method: "GET",
1569
1710
  headers: { Accept: "application/json" },
@@ -1603,6 +1744,226 @@ async function detectAtUriInHead(url, timeoutMs) {
1603
1744
  }
1604
1745
  }
1605
1746
 
1606
- export { CATEGORY_ORDER, COMPAT_FAMILIES, COMPAT_FAMILY_ORDER, DID_REQUIRED_WAYPOINTS, SUPPORTED_HOSTS, WAYPOINT_CATEGORIES_DATA, WAYPOINT_DESTINATIONS_DATA, WAYPOINT_ORDER, buildWaypointsForParsed, getCategorizedWaypointsData, getDisplayName, getFeaturedWaypointData, getRecommendedWaypointsData, getWaypointCountData, getWaypointDataForType, isSupportedHost, matchSupportedUrl, parseAtUri, parseURI, resolveAtUri, resolveHandle, resolveHandleStatus, resolveUrl, resolveViaApi, waypointActivity };
1747
+ // src/universalLinks.ts
1748
+ var UNIVERSAL_LINK_ORIGIN = "https://aturi.to";
1749
+ var OEMBED_PATH = "/api/oembed";
1750
+ var OEMBED_COLLECTION = "app.bsky.feed.post";
1751
+ var OEMBED_LINK_TYPE = "application/json+oembed";
1752
+ function buildUniversalLink(input, options = {}) {
1753
+ const parsed = parseTarget(input);
1754
+ if (!parsed) return null;
1755
+ const origin = normalizeOrigin(options.origin);
1756
+ const did = options.did ?? parsed.did;
1757
+ const { collection, rkey } = parsed;
1758
+ const identifier = options.preferDid && did ? did : parsed.handle;
1759
+ let path;
1760
+ if (collection && rkey) {
1761
+ if (collection === "app.bsky.feed.post") {
1762
+ path = `/profile/${seg(identifier)}/post/${seg(rkey)}`;
1763
+ } else if (collection === "app.bsky.graph.list") {
1764
+ path = `/profile/${seg(identifier)}/lists/${seg(rkey)}`;
1765
+ } else {
1766
+ path = `/profile/${seg(did || parsed.handle)}/${seg(collection)}/${seg(rkey)}`;
1767
+ }
1768
+ } else {
1769
+ path = `/profile/${seg(identifier)}`;
1770
+ }
1771
+ return `${origin}${path}${queryString(options.params)}`;
1772
+ }
1773
+ function parseUniversalLink(url, options = {}) {
1774
+ let target;
1775
+ try {
1776
+ target = typeof url === "string" ? new URL(url.trim()) : url;
1777
+ } catch {
1778
+ return null;
1779
+ }
1780
+ if (!/^https?:$/.test(target.protocol)) return null;
1781
+ let expectedHost;
1782
+ try {
1783
+ expectedHost = bareHost(new URL(normalizeOrigin(options.origin)).hostname);
1784
+ } catch {
1785
+ return null;
1786
+ }
1787
+ if (bareHost(target.hostname) !== expectedHost) return null;
1788
+ const probe = new URL(target.toString());
1789
+ probe.protocol = "https:";
1790
+ probe.hostname = "aturi.to";
1791
+ probe.port = "";
1792
+ const match = matchSupportedUrl(probe);
1793
+ if (match && (match.source === "aturi" || match.source === "aturiExplore")) {
1794
+ return normalizeParsed(match.parsed);
1795
+ }
1796
+ return parseEmbeddedAtUri(probe.pathname) ?? parsePath(probe.pathname);
1797
+ }
1798
+ function isUniversalLink(url, options = {}) {
1799
+ return parseUniversalLink(url, options) !== null;
1800
+ }
1801
+ function describeUniversalLink(input, options = {}) {
1802
+ const parsed = parseTarget(input);
1803
+ if (!parsed) return null;
1804
+ const url = buildUniversalLink(parsed, options);
1805
+ if (!url) return null;
1806
+ const did = options.did ?? parsed.did ?? null;
1807
+ const collection = parsed.collection ?? null;
1808
+ const rkey = parsed.rkey ?? null;
1809
+ const authority = did ?? parsed.handle;
1810
+ const atUri = collection && rkey ? `at://${authority}/${collection}/${rkey}` : `at://${authority}`;
1811
+ const label = describeTarget(parsed);
1812
+ return {
1813
+ url,
1814
+ atUri,
1815
+ type: parsed.type,
1816
+ handle: parsed.handle,
1817
+ did,
1818
+ collection,
1819
+ rkey,
1820
+ label,
1821
+ share: {
1822
+ title: options.title ?? label,
1823
+ text: options.text ?? label,
1824
+ url
1825
+ },
1826
+ snippets: {
1827
+ url,
1828
+ atUri,
1829
+ markdown: `[${label}](${url})`,
1830
+ html: `<a href="${escapeHtml(url)}">${escapeHtml(label)}</a>`
1831
+ },
1832
+ oembedUrl: buildOEmbedUrl(url, collection, options.origin)
1833
+ };
1834
+ }
1835
+ function buildUniversalLinkTags(input, options = {}) {
1836
+ const described = describeUniversalLink(input, options);
1837
+ if (!described) return null;
1838
+ const meta = [
1839
+ { name: "at:canonical", content: described.atUri }
1840
+ ];
1841
+ if (described.collection && described.rkey) {
1842
+ meta.push({
1843
+ name: "at:author",
1844
+ content: `at://${described.did ?? described.handle}`
1845
+ });
1846
+ }
1847
+ const link = [
1848
+ { rel: "alternate", href: described.atUri }
1849
+ ];
1850
+ if (described.oembedUrl) {
1851
+ link.push({
1852
+ rel: "alternate",
1853
+ type: OEMBED_LINK_TYPE,
1854
+ href: described.oembedUrl
1855
+ });
1856
+ }
1857
+ const html = [
1858
+ ...meta.map(
1859
+ (tag) => `<meta name="${escapeHtml(tag.name)}" content="${escapeHtml(tag.content)}" />`
1860
+ ),
1861
+ ...link.map((tag) => {
1862
+ const type = tag.type ? ` type="${escapeHtml(tag.type)}"` : "";
1863
+ return `<link rel="${escapeHtml(tag.rel)}"${type} href="${escapeHtml(tag.href)}" />`;
1864
+ })
1865
+ ].join("\n");
1866
+ return { meta, link, html };
1867
+ }
1868
+ function parseTarget(input) {
1869
+ if (typeof input !== "string") {
1870
+ return input && input.handle ? normalizeParsed(input) : null;
1871
+ }
1872
+ const trimmed = input.trim();
1873
+ if (!trimmed) return null;
1874
+ if (trimmed.toLowerCase().startsWith("at://")) {
1875
+ const parsed = parseAtUri(trimmed)?.parsed;
1876
+ return parsed ? normalizeParsed(parsed) : null;
1877
+ }
1878
+ if (/^https?:\/\//i.test(trimmed)) {
1879
+ let target;
1880
+ try {
1881
+ target = new URL(trimmed);
1882
+ } catch {
1883
+ return null;
1884
+ }
1885
+ const embedded = parseEmbeddedAtUri(target.pathname);
1886
+ if (embedded) return embedded;
1887
+ const matched = matchSupportedUrl(target)?.parsed;
1888
+ return matched ? normalizeParsed(matched) : null;
1889
+ }
1890
+ return parsePath(trimmed.startsWith("@") ? trimmed.slice(1) : trimmed);
1891
+ }
1892
+ function parsePath(path) {
1893
+ const segments = path.split("/").filter(Boolean);
1894
+ const handle = decodeSegment(segments[0] ?? "");
1895
+ if (!handle) return null;
1896
+ if (!handle.startsWith("did:") && !handle.includes(".")) return null;
1897
+ const parsed = parseAtUri(`at://${segments.join("/")}`)?.parsed;
1898
+ if (!parsed) return null;
1899
+ const collection = parsed.collection ? decodeSegment(parsed.collection) : void 0;
1900
+ const rkey = parsed.rkey ? decodeSegment(parsed.rkey) : void 0;
1901
+ return {
1902
+ ...parsed,
1903
+ handle,
1904
+ did: handle.startsWith("did:") ? handle : void 0,
1905
+ collection,
1906
+ rkey,
1907
+ uri: collection && rkey ? `at://${handle}/${collection}/${rkey}` : `at://${handle}`
1908
+ };
1909
+ }
1910
+ function normalizeParsed(parsed) {
1911
+ if (!parsed.handle.startsWith("@")) return parsed;
1912
+ const handle = parsed.handle.slice(1);
1913
+ if (!handle) return parsed;
1914
+ return {
1915
+ ...parsed,
1916
+ handle,
1917
+ uri: parsed.collection && parsed.rkey ? `at://${handle}/${parsed.collection}/${parsed.rkey}` : `at://${handle}`
1918
+ };
1919
+ }
1920
+ function parseEmbeddedAtUri(pathname) {
1921
+ const match = /^\/at:\/{1,2}(.+)$/i.exec(pathname);
1922
+ if (!match) return null;
1923
+ return parsePath(match[1]);
1924
+ }
1925
+ function buildOEmbedUrl(url, collection, origin) {
1926
+ if (collection !== OEMBED_COLLECTION) return null;
1927
+ return `${normalizeOrigin(origin)}${OEMBED_PATH}?url=${encodeURIComponent(url)}`;
1928
+ }
1929
+ function describeTarget(parsed) {
1930
+ const who = parsed.handle.startsWith("did:") ? parsed.handle : `@${parsed.handle}`;
1931
+ if (parsed.type === "post") return `Post by ${who}`;
1932
+ if (parsed.type === "list") return `List by ${who}`;
1933
+ if (parsed.collection) return `${parsed.collection} record by ${who}`;
1934
+ return who;
1935
+ }
1936
+ function normalizeOrigin(origin) {
1937
+ if (!origin) return UNIVERSAL_LINK_ORIGIN;
1938
+ return origin.trim().replace(/\/+$/, "") || UNIVERSAL_LINK_ORIGIN;
1939
+ }
1940
+ function bareHost(hostname) {
1941
+ return hostname.toLowerCase().replace(/^www\./, "");
1942
+ }
1943
+ function seg(value) {
1944
+ return encodeURIComponent(value).replace(/%3A/gi, ":");
1945
+ }
1946
+ function decodeSegment(value) {
1947
+ try {
1948
+ return decodeURIComponent(value);
1949
+ } catch {
1950
+ return value;
1951
+ }
1952
+ }
1953
+ function queryString(params) {
1954
+ if (!params) return "";
1955
+ const search = new URLSearchParams();
1956
+ for (const [key, value] of Object.entries(params)) {
1957
+ if (value === null || value === void 0 || value === "") continue;
1958
+ search.set(key, String(value));
1959
+ }
1960
+ const query = search.toString();
1961
+ return query ? `?${query}` : "";
1962
+ }
1963
+ function escapeHtml(value) {
1964
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1965
+ }
1966
+
1967
+ export { CATEGORY_ORDER, COMPAT_FAMILIES, COMPAT_FAMILY_ORDER, COMPOSE_INTENT_TEXT_PLACEHOLDER, DID_REQUIRED_WAYPOINTS, OEMBED_LINK_TYPE, SUPPORTED_HOSTS, UNIVERSAL_LINK_ORIGIN, WAYPOINT_CATEGORIES_DATA, WAYPOINT_DESTINATIONS_DATA, WAYPOINT_ORDER, 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 };
1607
1968
  //# sourceMappingURL=index.js.map
1608
1969
  //# sourceMappingURL=index.js.map