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