@aturi.to/waypoints 0.1.2 → 0.1.4

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.cjs CHANGED
@@ -77,6 +77,9 @@ var COMPAT_FAMILY_ORDER = [
77
77
  "blento",
78
78
  "atproto-explorer"
79
79
  ];
80
+ function socialAppComposeIntent(origin) {
81
+ return { url: `${origin}/intent/compose`, textParam: "text" };
82
+ }
80
83
  var WAYPOINT_DESTINATIONS_DATA = {
81
84
  aturi: {
82
85
  id: "aturi",
@@ -136,7 +139,17 @@ var WAYPOINT_DESTINATIONS_DATA = {
136
139
  },
137
140
  supportedTypes: ["post", "profile", "list", "record"],
138
141
  category: "blueskyClients",
139
- redirectCompat: ["bluesky-social", "standard-site"],
142
+ // Anisota implements the Bluesky shape deliberately, then extends it with
143
+ // `url`, `tags`, `channel`, and `title` (and answers `/post` with the same
144
+ // query). What's declared here is the interoperable subset every caller can
145
+ // rely on; the extras are Anisota-only and unrepresentable in this field.
146
+ composeIntent: socialAppComposeIntent("https://anisota.net"),
147
+ // Bluesky-shaped records only. anisota.net also reads Standard Site and
148
+ // Leaflet documents, but that role belongs to `anisotaReader`: listing both
149
+ // under `standard-site` put two entries for the same site in every
150
+ // "Publications" destination list, and this one's `expectedCollections`
151
+ // already says it does not claim those collections.
152
+ redirectCompat: ["bluesky-social"],
140
153
  expectedCollections: ["app.bsky.", "net.anisota."]
141
154
  },
142
155
  bluesky: {
@@ -152,12 +165,19 @@ var WAYPOINT_DESTINATIONS_DATA = {
152
165
  if (collection === "app.bsky.feed.post") {
153
166
  return `https://bsky.app/profile/${handle}/post/${rkey}`;
154
167
  }
168
+ if (collection === "app.bsky.graph.list") {
169
+ return `https://bsky.app/profile/${handle}/lists/${rkey}`;
170
+ }
155
171
  return `https://bsky.app/profile/${handle}`;
156
172
  }
157
173
  return `https://bsky.app/profile/${handle}`;
158
174
  },
159
175
  supportedTypes: ["post", "profile", "list", "record"],
160
176
  category: "blueskyClients",
177
+ composeIntent: {
178
+ ...socialAppComposeIntent("https://bsky.app"),
179
+ appUrl: "bluesky://intent/compose"
180
+ },
161
181
  redirectCompat: ["bluesky-social"],
162
182
  expectedCollections: ["app.bsky."]
163
183
  },
@@ -174,12 +194,16 @@ var WAYPOINT_DESTINATIONS_DATA = {
174
194
  if (collection === "app.bsky.feed.post") {
175
195
  return `https://blacksky.community/profile/${handle}/post/${rkey}`;
176
196
  }
197
+ if (collection === "app.bsky.graph.list") {
198
+ return `https://blacksky.community/profile/${handle}/lists/${rkey}`;
199
+ }
177
200
  return `https://blacksky.community/profile/${handle}`;
178
201
  }
179
202
  return `https://blacksky.community/profile/${handle}`;
180
203
  },
181
204
  supportedTypes: ["post", "profile", "list", "record"],
182
205
  category: "blueskyForks",
206
+ composeIntent: socialAppComposeIntent("https://blacksky.community"),
183
207
  redirectCompat: ["bluesky-social"],
184
208
  expectedCollections: ["app.bsky."]
185
209
  },
@@ -188,7 +212,6 @@ var WAYPOINT_DESTINATIONS_DATA = {
188
212
  name: "Red Dwarf",
189
213
  description: (collection) => {
190
214
  if (collection === "app.bsky.feed.post") return "View post on reddwarf.app";
191
- if (collection === "app.bsky.graph.list") return "View list on reddwarf.app";
192
215
  return "View profile on reddwarf.app";
193
216
  },
194
217
  getUrl: (handle, collection, rkey) => {
@@ -200,7 +223,10 @@ var WAYPOINT_DESTINATIONS_DATA = {
200
223
  }
201
224
  return `https://reddwarf.app/profile/${handle}`;
202
225
  },
203
- supportedTypes: ["post", "profile", "list", "record"],
226
+ // No `list`: unlike the social-app forks in this family, Red Dwarf has no
227
+ // `/profile/:handle/lists/:rkey` route, so offering it for a list would
228
+ // silently land the user on the list's author instead.
229
+ supportedTypes: ["post", "profile", "record"],
204
230
  category: "blueskyClients",
205
231
  redirectCompat: ["bluesky-social"],
206
232
  expectedCollections: ["app.bsky."]
@@ -227,6 +253,33 @@ var WAYPOINT_DESTINATIONS_DATA = {
227
253
  },
228
254
  supportedTypes: ["post", "profile", "list", "record"],
229
255
  category: "blueskyClients",
256
+ // Impro routes /intent/compose to its home view, which opens the composer
257
+ // for a signed-in user — but it never reads `?text`, so no textParam here.
258
+ composeIntent: { url: "https://impro.social/intent/compose" },
259
+ redirectCompat: ["bluesky-social"],
260
+ expectedCollections: ["app.bsky."]
261
+ },
262
+ lea: {
263
+ id: "lea",
264
+ name: "Lea",
265
+ description: (collection) => {
266
+ if (collection === "app.bsky.feed.post") return "View post on lea.ac";
267
+ return "View profile on lea.ac";
268
+ },
269
+ getUrl: (handle, collection, rkey) => {
270
+ if (collection && rkey) {
271
+ if (collection === "app.bsky.feed.post") {
272
+ return `https://lea.ac/profile/${handle}/post/${rkey}`;
273
+ }
274
+ return `https://lea.ac/profile/${handle}`;
275
+ }
276
+ return `https://lea.ac/profile/${handle}`;
277
+ },
278
+ // No `list`: like Red Dwarf, Lea serves only `/profile/:handle` and
279
+ // `/profile/:handle/post/:rkey` — `/lists/:rkey` 404s — so claiming list
280
+ // support would drop the user on an error page instead of the list.
281
+ supportedTypes: ["post", "profile", "record"],
282
+ category: "blueskyClients",
230
283
  redirectCompat: ["bluesky-social"],
231
284
  expectedCollections: ["app.bsky."]
232
285
  },
@@ -327,12 +380,16 @@ var WAYPOINT_DESTINATIONS_DATA = {
327
380
  if (collection === "app.bsky.feed.post") {
328
381
  return `https://witchsky.app/profile/${handle}/post/${rkey}`;
329
382
  }
383
+ if (collection === "app.bsky.graph.list") {
384
+ return `https://witchsky.app/profile/${handle}/lists/${rkey}`;
385
+ }
330
386
  return `https://witchsky.app/profile/${handle}`;
331
387
  }
332
388
  return `https://witchsky.app/profile/${handle}`;
333
389
  },
334
390
  supportedTypes: ["post", "profile", "list", "record"],
335
391
  category: "blueskyForks",
392
+ composeIntent: socialAppComposeIntent("https://witchsky.app"),
336
393
  redirectCompat: ["bluesky-social"],
337
394
  expectedCollections: ["app.bsky."]
338
395
  },
@@ -349,12 +406,16 @@ var WAYPOINT_DESTINATIONS_DATA = {
349
406
  if (collection === "app.bsky.feed.post") {
350
407
  return `https://deer.social/profile/${handle}/post/${rkey}`;
351
408
  }
409
+ if (collection === "app.bsky.graph.list") {
410
+ return `https://deer.social/profile/${handle}/lists/${rkey}`;
411
+ }
352
412
  return `https://deer.social/profile/${handle}`;
353
413
  }
354
414
  return `https://deer.social/profile/${handle}`;
355
415
  },
356
416
  supportedTypes: ["post", "profile", "list", "record"],
357
417
  category: "blueskyForks",
418
+ composeIntent: socialAppComposeIntent("https://deer.social"),
358
419
  redirectCompat: ["bluesky-social"],
359
420
  expectedCollections: ["app.bsky."]
360
421
  },
@@ -371,12 +432,42 @@ var WAYPOINT_DESTINATIONS_DATA = {
371
432
  if (collection === "app.bsky.feed.post") {
372
433
  return `https://mu.social/profile/${handle}/post/${rkey}`;
373
434
  }
435
+ if (collection === "app.bsky.graph.list") {
436
+ return `https://mu.social/profile/${handle}/lists/${rkey}`;
437
+ }
374
438
  return `https://mu.social/profile/${handle}`;
375
439
  }
376
440
  return `https://mu.social/profile/${handle}`;
377
441
  },
378
442
  supportedTypes: ["post", "profile", "list", "record"],
379
443
  category: "blueskyForks",
444
+ composeIntent: socialAppComposeIntent("https://mu.social"),
445
+ redirectCompat: ["bluesky-social"],
446
+ expectedCollections: ["app.bsky."]
447
+ },
448
+ northsky: {
449
+ id: "northsky",
450
+ name: "Northsky",
451
+ description: (collection) => {
452
+ if (collection === "app.bsky.feed.post") return "View post on northsky.app";
453
+ if (collection === "app.bsky.graph.list") return "View list on northsky.app";
454
+ return "View profile on northsky.app";
455
+ },
456
+ getUrl: (handle, collection, rkey) => {
457
+ if (collection && rkey) {
458
+ if (collection === "app.bsky.feed.post") {
459
+ return `https://northsky.app/profile/${handle}/post/${rkey}`;
460
+ }
461
+ if (collection === "app.bsky.graph.list") {
462
+ return `https://northsky.app/profile/${handle}/lists/${rkey}`;
463
+ }
464
+ return `https://northsky.app/profile/${handle}`;
465
+ }
466
+ return `https://northsky.app/profile/${handle}`;
467
+ },
468
+ supportedTypes: ["post", "profile", "list", "record"],
469
+ category: "blueskyForks",
470
+ composeIntent: socialAppComposeIntent("https://northsky.app"),
380
471
  redirectCompat: ["bluesky-social"],
381
472
  expectedCollections: ["app.bsky."]
382
473
  },
@@ -655,7 +746,9 @@ var WAYPOINT_ORDER = [
655
746
  "taproot",
656
747
  "witchsky",
657
748
  "mu",
658
- "deer"
749
+ "deer",
750
+ "lea",
751
+ "northsky"
659
752
  ];
660
753
  function getWaypointDataForType(type) {
661
754
  return WAYPOINT_ORDER.map((id) => WAYPOINT_DESTINATIONS_DATA[id]).filter((waypoint) => waypoint.supportedTypes.includes(type));
@@ -822,6 +915,55 @@ function waypointActivity(waypoint, repoCollections) {
822
915
  }
823
916
  return "absent";
824
917
  }
918
+ var COMPOSE_INTENT_TEXT_PLACEHOLDER = "{text}";
919
+ function supportsComposeIntent(waypoint) {
920
+ return !!waypoint.composeIntent;
921
+ }
922
+ function getComposeIntentUrl(waypoint, text) {
923
+ const intent = waypoint.composeIntent;
924
+ if (!intent) return null;
925
+ return appendComposeText(intent.url, intent.textParam, text);
926
+ }
927
+ function getComposeIntentAppUrl(waypoint, text) {
928
+ const intent = waypoint.composeIntent;
929
+ if (!intent?.appUrl) return null;
930
+ return appendComposeText(intent.appUrl, intent.textParam, text);
931
+ }
932
+ function getComposeIntentTemplate(waypoint) {
933
+ const intent = waypoint.composeIntent;
934
+ if (!intent) return null;
935
+ if (!intent.textParam) return intent.url;
936
+ return `${intent.url}?${intent.textParam}=${COMPOSE_INTENT_TEXT_PLACEHOLDER}`;
937
+ }
938
+ function getComposeIntentWaypoints(type) {
939
+ const waypoints = type ? getWaypointDataForType(type) : WAYPOINT_ORDER.map((id) => WAYPOINT_DESTINATIONS_DATA[id]).filter(Boolean);
940
+ return waypoints.filter(supportsComposeIntent);
941
+ }
942
+ function describeComposeIntent(waypoint, text) {
943
+ const intent = waypoint.composeIntent;
944
+ if (!intent) return null;
945
+ const descriptor = {
946
+ url: getComposeIntentUrl(waypoint, text),
947
+ urlTemplate: getComposeIntentTemplate(waypoint),
948
+ textParam: intent.textParam ?? null,
949
+ prefillsText: !!intent.textParam
950
+ };
951
+ const appUrl = getComposeIntentAppUrl(waypoint, text);
952
+ if (appUrl) descriptor.appUrl = appUrl;
953
+ return descriptor;
954
+ }
955
+ function appendComposeText(base, textParam, text) {
956
+ if (!textParam || !text) return base;
957
+ return `${base}?${textParam}=${encodeURIComponent(text)}`;
958
+ }
959
+
960
+ // src/requestDeadline.ts
961
+ var UPSTREAM_USER_AGENT = "aturi.to (+https://aturi.to/mcp)";
962
+ function identifyingHeaders(existing) {
963
+ const headers = existing;
964
+ if (typeof window !== "undefined") return headers;
965
+ return { ...headers, "User-Agent": UPSTREAM_USER_AGENT };
966
+ }
825
967
 
826
968
  // src/upstreamFetch.ts
827
969
  var RETRY_DELAY_MS = 250;
@@ -829,8 +971,10 @@ async function upstreamFetch(url, { timeoutMs = 8e3, retries = 1, ...init } = {}
829
971
  let lastError;
830
972
  for (let attempt = 0; attempt <= retries; attempt++) {
831
973
  try {
974
+ const headers = identifyingHeaders(init.headers);
832
975
  return await fetch(url, {
833
976
  ...init,
977
+ ...headers ? { headers } : {},
834
978
  signal: AbortSignal.timeout(timeoutMs)
835
979
  });
836
980
  } catch (error) {
@@ -861,6 +1005,14 @@ function parseURI(handle, collection, rkey) {
861
1005
  did: handle.startsWith("did:") ? handle : void 0
862
1006
  };
863
1007
  }
1008
+ if (collection === "space") {
1009
+ return {
1010
+ type: "unknown",
1011
+ uri: "",
1012
+ handle,
1013
+ error: "Space URIs are not public records"
1014
+ };
1015
+ }
864
1016
  if (collection && rkey) {
865
1017
  let type = "record";
866
1018
  if (collection === "app.bsky.feed.post") {
@@ -920,8 +1072,10 @@ var BLUESKY_FAMILY = [
920
1072
  { source: "blacksky", hosts: ["blacksky.community"] },
921
1073
  { source: "reddwarf", hosts: ["reddwarf.app"] },
922
1074
  { source: "impro", hosts: ["impro.social"] },
1075
+ { source: "lea", hosts: ["lea.ac"] },
923
1076
  { source: "witchsky", hosts: ["witchsky.app"] },
924
1077
  { source: "deer", hosts: ["deer.social"] },
1078
+ { source: "northsky", hosts: ["northsky.app"] },
925
1079
  { source: "mu", hosts: ["mu.social"] },
926
1080
  { source: "anisota", hosts: ["anisota.net"], matchSubdomains: true }
927
1081
  ];
@@ -1108,6 +1262,7 @@ function parseAtUriPath(pathname) {
1108
1262
  const rest = match[1];
1109
1263
  const segments = rest.split("/").filter(Boolean);
1110
1264
  if (segments.length === 0) return null;
1265
+ if (segments[1] === "space") return null;
1111
1266
  return {
1112
1267
  handle: segments[0],
1113
1268
  collection: segments[1],
@@ -1317,6 +1472,7 @@ function matchTaproot(host, pathname) {
1317
1472
  if (!m) return null;
1318
1473
  const segments = m[1].split("/").filter(Boolean);
1319
1474
  if (segments.length === 0) return null;
1475
+ if (segments[1] === "space") return null;
1320
1476
  const handle = segments[0];
1321
1477
  const collection = segments[1];
1322
1478
  const rkey = segments[2];
@@ -1341,8 +1497,9 @@ function matchTaproot(host, pathname) {
1341
1497
  }
1342
1498
  function matchAturi(host, parts) {
1343
1499
  if (host !== "aturi.to") return null;
1344
- const isIdentifier = (seg) => !!seg && (seg.startsWith("did:") || seg.includes("."));
1500
+ const isIdentifier = (seg2) => !!seg2 && (seg2.startsWith("did:") || seg2.includes("."));
1345
1501
  if (parts[0] === "explore" && isIdentifier(parts[1])) {
1502
+ if (parts[2] === "space") return null;
1346
1503
  const handle = parts[1];
1347
1504
  const did = handle.startsWith("did:") ? handle : void 0;
1348
1505
  const collection = parts[2];
@@ -1425,6 +1582,7 @@ function parseAtUri(uri) {
1425
1582
  const rest = uri.slice("at://".length);
1426
1583
  const segments = rest.split("/").filter(Boolean);
1427
1584
  if (segments.length === 0) return null;
1585
+ if (segments[1] === "space") return null;
1428
1586
  const handle = segments[0];
1429
1587
  const did = handle.startsWith("did:") ? handle : void 0;
1430
1588
  const collection = segments[1];
@@ -1490,18 +1648,26 @@ function buildWaypointsForParsed(parsed, options = {}) {
1490
1648
  if (DID_REQUIRED_WAYPOINTS.has(w.id) && !did) return null;
1491
1649
  const url = w.getUrl(parsed.handle, parsed.collection, parsed.rkey, did);
1492
1650
  if (!url) return null;
1493
- return { id: w.id, name: w.name, category: w.category, url };
1651
+ return {
1652
+ id: w.id,
1653
+ name: w.name,
1654
+ category: w.category,
1655
+ url,
1656
+ composeIntent: describeComposeIntent(w, options.composeText)
1657
+ };
1494
1658
  }).filter((w) => !!w);
1495
1659
  const recommendedRaw = getRecommendedWaypointsData(type, parsed.collection);
1496
1660
  const availableIds = new Set(waypoints.map((w) => w.id));
1497
1661
  const ids = recommendedRaw.waypoints.map((w) => w.id).filter((id) => id !== exclude && availableIds.has(id));
1498
1662
  return { waypoints, recommended: { ids, label: recommendedRaw.label } };
1499
1663
  }
1500
- function resolveAtUri(uri) {
1664
+ function resolveAtUri(uri, options = {}) {
1501
1665
  const match = parseAtUri(uri);
1502
1666
  if (!match) return null;
1503
1667
  const { parsed, source } = match;
1504
- const { waypoints, recommended } = buildWaypointsForParsed(parsed);
1668
+ const { waypoints, recommended } = buildWaypointsForParsed(parsed, {
1669
+ composeText: options.composeText
1670
+ });
1505
1671
  return {
1506
1672
  parsed,
1507
1673
  source,
@@ -1543,7 +1709,8 @@ async function resolveUrl(url, options = {}) {
1543
1709
  }
1544
1710
  const { waypoints, recommended } = buildWaypointsForParsed(parsed, {
1545
1711
  did,
1546
- excludeSourceId: source
1712
+ excludeSourceId: source,
1713
+ composeText: options.composeText
1547
1714
  });
1548
1715
  return {
1549
1716
  parsed: { ...parsed, did },
@@ -1566,6 +1733,7 @@ async function resolveViaApi(input, options = {}) {
1566
1733
  throw new Error("resolveViaApi requires either `url` or `atUri`");
1567
1734
  }
1568
1735
  if (input.headDetect === false) params.set("headDetect", "false");
1736
+ if (input.composeText) params.set("composeText", input.composeText);
1569
1737
  const res = await fetchImpl(`${endpoint}?${params.toString()}`, {
1570
1738
  method: "GET",
1571
1739
  headers: { Accept: "application/json" },
@@ -1605,30 +1773,264 @@ async function detectAtUriInHead(url, timeoutMs) {
1605
1773
  }
1606
1774
  }
1607
1775
 
1776
+ // src/universalLinks.ts
1777
+ var UNIVERSAL_LINK_ORIGIN = "https://aturi.to";
1778
+ var OEMBED_PATH = "/api/oembed";
1779
+ var OEMBED_COLLECTION = "app.bsky.feed.post";
1780
+ var OEMBED_LINK_TYPE = "application/json+oembed";
1781
+ function buildUniversalLink(input, options = {}) {
1782
+ const parsed = parseTarget(input);
1783
+ if (!parsed) return null;
1784
+ const origin = normalizeOrigin(options.origin);
1785
+ const did = options.did ?? parsed.did;
1786
+ const { collection, rkey } = parsed;
1787
+ const identifier = options.preferDid && did ? did : parsed.handle;
1788
+ let path;
1789
+ if (collection && rkey) {
1790
+ if (collection === "app.bsky.feed.post") {
1791
+ path = `/profile/${seg(identifier)}/post/${seg(rkey)}`;
1792
+ } else if (collection === "app.bsky.graph.list") {
1793
+ path = `/profile/${seg(identifier)}/lists/${seg(rkey)}`;
1794
+ } else {
1795
+ path = `/profile/${seg(did || parsed.handle)}/${seg(collection)}/${seg(rkey)}`;
1796
+ }
1797
+ } else {
1798
+ path = `/profile/${seg(identifier)}`;
1799
+ }
1800
+ return `${origin}${path}${queryString(options.params)}`;
1801
+ }
1802
+ function parseUniversalLink(url, options = {}) {
1803
+ let target;
1804
+ try {
1805
+ target = typeof url === "string" ? new URL(url.trim()) : url;
1806
+ } catch {
1807
+ return null;
1808
+ }
1809
+ if (!/^https?:$/.test(target.protocol)) return null;
1810
+ let expectedHost;
1811
+ try {
1812
+ expectedHost = bareHost(new URL(normalizeOrigin(options.origin)).hostname);
1813
+ } catch {
1814
+ return null;
1815
+ }
1816
+ if (bareHost(target.hostname) !== expectedHost) return null;
1817
+ const probe = new URL(target.toString());
1818
+ probe.protocol = "https:";
1819
+ probe.hostname = "aturi.to";
1820
+ probe.port = "";
1821
+ const match = matchSupportedUrl(probe);
1822
+ if (match && (match.source === "aturi" || match.source === "aturiExplore")) {
1823
+ return normalizeParsed(match.parsed);
1824
+ }
1825
+ return parseEmbeddedAtUri(probe.pathname) ?? parsePath(probe.pathname);
1826
+ }
1827
+ function isUniversalLink(url, options = {}) {
1828
+ return parseUniversalLink(url, options) !== null;
1829
+ }
1830
+ function describeUniversalLink(input, options = {}) {
1831
+ const parsed = parseTarget(input);
1832
+ if (!parsed) return null;
1833
+ const url = buildUniversalLink(parsed, options);
1834
+ if (!url) return null;
1835
+ const did = options.did ?? parsed.did ?? null;
1836
+ const collection = parsed.collection ?? null;
1837
+ const rkey = parsed.rkey ?? null;
1838
+ const authority = did ?? parsed.handle;
1839
+ const atUri = collection && rkey ? `at://${authority}/${collection}/${rkey}` : `at://${authority}`;
1840
+ const label = describeTarget(parsed);
1841
+ return {
1842
+ url,
1843
+ atUri,
1844
+ type: parsed.type,
1845
+ handle: parsed.handle,
1846
+ did,
1847
+ collection,
1848
+ rkey,
1849
+ label,
1850
+ share: {
1851
+ title: options.title ?? label,
1852
+ text: options.text ?? label,
1853
+ url
1854
+ },
1855
+ snippets: {
1856
+ url,
1857
+ atUri,
1858
+ markdown: `[${label}](${url})`,
1859
+ html: `<a href="${escapeHtml(url)}">${escapeHtml(label)}</a>`
1860
+ },
1861
+ oembedUrl: buildOEmbedUrl(url, collection, options.origin)
1862
+ };
1863
+ }
1864
+ function buildUniversalLinkTags(input, options = {}) {
1865
+ const described = describeUniversalLink(input, options);
1866
+ if (!described) return null;
1867
+ const meta = [
1868
+ { name: "at:canonical", content: described.atUri }
1869
+ ];
1870
+ if (described.collection && described.rkey) {
1871
+ meta.push({
1872
+ name: "at:author",
1873
+ content: `at://${described.did ?? described.handle}`
1874
+ });
1875
+ }
1876
+ const link = [
1877
+ { rel: "alternate", href: described.atUri }
1878
+ ];
1879
+ if (described.oembedUrl) {
1880
+ link.push({
1881
+ rel: "alternate",
1882
+ type: OEMBED_LINK_TYPE,
1883
+ href: described.oembedUrl
1884
+ });
1885
+ }
1886
+ const html = [
1887
+ ...meta.map(
1888
+ (tag) => `<meta name="${escapeHtml(tag.name)}" content="${escapeHtml(tag.content)}" />`
1889
+ ),
1890
+ ...link.map((tag) => {
1891
+ const type = tag.type ? ` type="${escapeHtml(tag.type)}"` : "";
1892
+ return `<link rel="${escapeHtml(tag.rel)}"${type} href="${escapeHtml(tag.href)}" />`;
1893
+ })
1894
+ ].join("\n");
1895
+ return { meta, link, html };
1896
+ }
1897
+ function parseTarget(input) {
1898
+ if (typeof input !== "string") {
1899
+ return input && input.handle ? normalizeParsed(input) : null;
1900
+ }
1901
+ const trimmed = input.trim();
1902
+ if (!trimmed) return null;
1903
+ if (trimmed.toLowerCase().startsWith("at://")) {
1904
+ const parsed = parseAtUri(trimmed)?.parsed;
1905
+ return parsed ? normalizeParsed(parsed) : null;
1906
+ }
1907
+ if (/^https?:\/\//i.test(trimmed)) {
1908
+ let target;
1909
+ try {
1910
+ target = new URL(trimmed);
1911
+ } catch {
1912
+ return null;
1913
+ }
1914
+ const embedded = parseEmbeddedAtUri(target.pathname);
1915
+ if (embedded) return embedded;
1916
+ const matched = matchSupportedUrl(target)?.parsed;
1917
+ return matched ? normalizeParsed(matched) : null;
1918
+ }
1919
+ return parsePath(trimmed.startsWith("@") ? trimmed.slice(1) : trimmed);
1920
+ }
1921
+ function parsePath(path) {
1922
+ const segments = path.split("/").filter(Boolean);
1923
+ const handle = decodeSegment(segments[0] ?? "");
1924
+ if (!handle) return null;
1925
+ if (!handle.startsWith("did:") && !handle.includes(".")) return null;
1926
+ const parsed = parseAtUri(`at://${segments.join("/")}`)?.parsed;
1927
+ if (!parsed) return null;
1928
+ const collection = parsed.collection ? decodeSegment(parsed.collection) : void 0;
1929
+ const rkey = parsed.rkey ? decodeSegment(parsed.rkey) : void 0;
1930
+ return {
1931
+ ...parsed,
1932
+ handle,
1933
+ did: handle.startsWith("did:") ? handle : void 0,
1934
+ collection,
1935
+ rkey,
1936
+ uri: collection && rkey ? `at://${handle}/${collection}/${rkey}` : `at://${handle}`
1937
+ };
1938
+ }
1939
+ function normalizeParsed(parsed) {
1940
+ if (!parsed.handle.startsWith("@")) return parsed;
1941
+ const handle = parsed.handle.slice(1);
1942
+ if (!handle) return parsed;
1943
+ return {
1944
+ ...parsed,
1945
+ handle,
1946
+ uri: parsed.collection && parsed.rkey ? `at://${handle}/${parsed.collection}/${parsed.rkey}` : `at://${handle}`
1947
+ };
1948
+ }
1949
+ function parseEmbeddedAtUri(pathname) {
1950
+ const match = /^\/at:\/{1,2}(.+)$/i.exec(pathname);
1951
+ if (!match) return null;
1952
+ return parsePath(match[1]);
1953
+ }
1954
+ function buildOEmbedUrl(url, collection, origin) {
1955
+ if (collection !== OEMBED_COLLECTION) return null;
1956
+ return `${normalizeOrigin(origin)}${OEMBED_PATH}?url=${encodeURIComponent(url)}`;
1957
+ }
1958
+ function describeTarget(parsed) {
1959
+ const who = parsed.handle.startsWith("did:") ? parsed.handle : `@${parsed.handle}`;
1960
+ if (parsed.type === "post") return `Post by ${who}`;
1961
+ if (parsed.type === "list") return `List by ${who}`;
1962
+ if (parsed.collection) return `${parsed.collection} record by ${who}`;
1963
+ return who;
1964
+ }
1965
+ function normalizeOrigin(origin) {
1966
+ if (!origin) return UNIVERSAL_LINK_ORIGIN;
1967
+ return origin.trim().replace(/\/+$/, "") || UNIVERSAL_LINK_ORIGIN;
1968
+ }
1969
+ function bareHost(hostname) {
1970
+ return hostname.toLowerCase().replace(/^www\./, "");
1971
+ }
1972
+ function seg(value) {
1973
+ return encodeURIComponent(value).replace(/%3A/gi, ":");
1974
+ }
1975
+ function decodeSegment(value) {
1976
+ try {
1977
+ return decodeURIComponent(value);
1978
+ } catch {
1979
+ return value;
1980
+ }
1981
+ }
1982
+ function queryString(params) {
1983
+ if (!params) return "";
1984
+ const search = new URLSearchParams();
1985
+ for (const [key, value] of Object.entries(params)) {
1986
+ if (value === null || value === void 0 || value === "") continue;
1987
+ search.set(key, String(value));
1988
+ }
1989
+ const query = search.toString();
1990
+ return query ? `?${query}` : "";
1991
+ }
1992
+ function escapeHtml(value) {
1993
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1994
+ }
1995
+
1608
1996
  exports.CATEGORY_ORDER = CATEGORY_ORDER;
1609
1997
  exports.COMPAT_FAMILIES = COMPAT_FAMILIES;
1610
1998
  exports.COMPAT_FAMILY_ORDER = COMPAT_FAMILY_ORDER;
1999
+ exports.COMPOSE_INTENT_TEXT_PLACEHOLDER = COMPOSE_INTENT_TEXT_PLACEHOLDER;
1611
2000
  exports.DID_REQUIRED_WAYPOINTS = DID_REQUIRED_WAYPOINTS;
2001
+ exports.OEMBED_LINK_TYPE = OEMBED_LINK_TYPE;
1612
2002
  exports.SUPPORTED_HOSTS = SUPPORTED_HOSTS;
2003
+ exports.UNIVERSAL_LINK_ORIGIN = UNIVERSAL_LINK_ORIGIN;
1613
2004
  exports.WAYPOINT_CATEGORIES_DATA = WAYPOINT_CATEGORIES_DATA;
1614
2005
  exports.WAYPOINT_DESTINATIONS_DATA = WAYPOINT_DESTINATIONS_DATA;
1615
2006
  exports.WAYPOINT_ORDER = WAYPOINT_ORDER;
2007
+ exports.buildUniversalLink = buildUniversalLink;
2008
+ exports.buildUniversalLinkTags = buildUniversalLinkTags;
1616
2009
  exports.buildWaypointsForParsed = buildWaypointsForParsed;
2010
+ exports.describeComposeIntent = describeComposeIntent;
2011
+ exports.describeUniversalLink = describeUniversalLink;
1617
2012
  exports.getCategorizedWaypointsData = getCategorizedWaypointsData;
2013
+ exports.getComposeIntentAppUrl = getComposeIntentAppUrl;
2014
+ exports.getComposeIntentTemplate = getComposeIntentTemplate;
2015
+ exports.getComposeIntentUrl = getComposeIntentUrl;
2016
+ exports.getComposeIntentWaypoints = getComposeIntentWaypoints;
1618
2017
  exports.getDisplayName = getDisplayName;
1619
2018
  exports.getFeaturedWaypointData = getFeaturedWaypointData;
1620
2019
  exports.getRecommendedWaypointsData = getRecommendedWaypointsData;
1621
2020
  exports.getWaypointCountData = getWaypointCountData;
1622
2021
  exports.getWaypointDataForType = getWaypointDataForType;
1623
2022
  exports.isSupportedHost = isSupportedHost;
2023
+ exports.isUniversalLink = isUniversalLink;
1624
2024
  exports.matchSupportedUrl = matchSupportedUrl;
1625
2025
  exports.parseAtUri = parseAtUri;
1626
2026
  exports.parseURI = parseURI;
2027
+ exports.parseUniversalLink = parseUniversalLink;
1627
2028
  exports.resolveAtUri = resolveAtUri;
1628
2029
  exports.resolveHandle = resolveHandle;
1629
2030
  exports.resolveHandleStatus = resolveHandleStatus;
1630
2031
  exports.resolveUrl = resolveUrl;
1631
2032
  exports.resolveViaApi = resolveViaApi;
2033
+ exports.supportsComposeIntent = supportsComposeIntent;
1632
2034
  exports.waypointActivity = waypointActivity;
1633
2035
  //# sourceMappingURL=index.cjs.map
1634
2036
  //# sourceMappingURL=index.cjs.map