@aturi.to/waypoints 0.1.1 → 0.1.2

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
@@ -1,6 +1,6 @@
1
1
  # @aturi.to/waypoints
2
2
 
3
- Aturi's curated catalog of Atmosphere (AT Protocol) clients "waypoints" plus
3
+ Aturi's curated catalog of Atmosphere (AT Protocol) clients ("waypoints") plus
4
4
  the logic to turn an AT URI into per-client "Open in…" links, recommend the best
5
5
  client for a record type, and reverse-resolve a pasted URL back into an AT URI.
6
6
 
@@ -9,7 +9,7 @@ Ships ESM + CJS with full type definitions.
9
9
 
10
10
  For a drop-in React picker UI, see [`@aturi.to/waypoints-react`](../waypoints-react).
11
11
 
12
- > **Beta early release.** This is a `0.x` package that hasn't been
12
+ > **Beta: early release.** This is a `0.x` package that hasn't been
13
13
  > thoroughly tested in production yet. Expect rough edges, and possible
14
14
  > breaking changes between minor versions while the API settles. Bug reports
15
15
  > and feedback are very welcome at
@@ -48,20 +48,20 @@ const fromUrl = await resolveUrl('https://bsky.app/profile/alice.bsky.social/pos
48
48
  - `buildWaypointsForParsed(parsed, { did?, excludeSourceId? })`
49
49
  - `resolveAtUri(uri)`
50
50
  - `resolveUrl(url, { fetchHead?, resolveHandle? })`
51
- - `resolveViaApi(input, { endpoint? })` typed client for the hosted
51
+ - `resolveViaApi(input, { endpoint? })`: typed client for the hosted
52
52
  `aturi.to/api/resolve` endpoint.
53
53
 
54
54
  ### DID-only waypoints
55
55
 
56
56
  A handful of destinations (`pdsls`, `atptools`, `margin`, `grain`, `popfeed`)
57
57
  only produce useful URLs when a DID is known. They're filtered out unless a DID
58
- is available pass one in, or supply a `resolveHandle` to `resolveUrl`.
58
+ is available: pass one in, or supply a `resolveHandle` to `resolveUrl`.
59
59
 
60
60
  ### Hosted vs. local resolution
61
61
 
62
62
  `resolveUrl` matches URL patterns locally (no network). The optional `fetchHead`
63
63
  flag and `resolveViaApi` let you fall back to fetching the page and probing for a
64
- `<link href="at://…">` useful for sites without a recognizable URL shape.
64
+ `<link href="at://…">`, useful for sites without a recognizable URL shape.
65
65
  `resolveViaApi` is the right choice from a browser, where fetching arbitrary
66
66
  pages is blocked by CORS.
67
67
 
package/dist/index.cjs CHANGED
@@ -56,6 +56,11 @@ var COMPAT_FAMILIES = {
56
56
  id: "blento",
57
57
  name: "Blento",
58
58
  description: "Blento profiles."
59
+ },
60
+ "atproto-explorer": {
61
+ id: "atproto-explorer",
62
+ name: "Record explorers",
63
+ description: "Raw AT Protocol record explorers (pdsls, atp.tools, Aturi Explore) that render any record by its AT URI."
59
64
  }
60
65
  };
61
66
  var COMPAT_FAMILY_ORDER = [
@@ -69,7 +74,8 @@ var COMPAT_FAMILY_ORDER = [
69
74
  "streamplace",
70
75
  "popfeed",
71
76
  "sifa",
72
- "blento"
77
+ "blento",
78
+ "atproto-explorer"
73
79
  ];
74
80
  var WAYPOINT_DESTINATIONS_DATA = {
75
81
  aturi: {
@@ -195,7 +201,32 @@ var WAYPOINT_DESTINATIONS_DATA = {
195
201
  return `https://reddwarf.app/profile/${handle}`;
196
202
  },
197
203
  supportedTypes: ["post", "profile", "list", "record"],
198
- category: "blueskyForks",
204
+ category: "blueskyClients",
205
+ redirectCompat: ["bluesky-social"],
206
+ expectedCollections: ["app.bsky."]
207
+ },
208
+ impro: {
209
+ id: "impro",
210
+ name: "Impro",
211
+ description: (collection) => {
212
+ if (collection === "app.bsky.feed.post") return "View post on impro.social";
213
+ if (collection === "app.bsky.graph.list") return "View list on impro.social";
214
+ return "View profile on impro.social";
215
+ },
216
+ getUrl: (handle, collection, rkey) => {
217
+ if (collection && rkey) {
218
+ if (collection === "app.bsky.feed.post") {
219
+ return `https://impro.social/profile/${handle}/post/${rkey}`;
220
+ }
221
+ if (collection === "app.bsky.graph.list") {
222
+ return `https://impro.social/profile/${handle}/lists/${rkey}`;
223
+ }
224
+ return `https://impro.social/profile/${handle}`;
225
+ }
226
+ return `https://impro.social/profile/${handle}`;
227
+ },
228
+ supportedTypes: ["post", "profile", "list", "record"],
229
+ category: "blueskyClients",
199
230
  redirectCompat: ["bluesky-social"],
200
231
  expectedCollections: ["app.bsky."]
201
232
  },
@@ -224,7 +255,7 @@ var WAYPOINT_DESTINATIONS_DATA = {
224
255
  },
225
256
  supportedTypes: ["post", "profile", "list", "record"],
226
257
  category: "devTools",
227
- redirectCompat: []
258
+ redirectCompat: ["atproto-explorer"]
228
259
  },
229
260
  aturiExplore: {
230
261
  id: "aturiExplore",
@@ -245,7 +276,7 @@ var WAYPOINT_DESTINATIONS_DATA = {
245
276
  },
246
277
  supportedTypes: ["post", "profile", "list", "record"],
247
278
  category: "devTools",
248
- redirectCompat: []
279
+ redirectCompat: ["atproto-explorer"]
249
280
  },
250
281
  atptools: {
251
282
  id: "atptools",
@@ -260,7 +291,7 @@ var WAYPOINT_DESTINATIONS_DATA = {
260
291
  },
261
292
  supportedTypes: ["post", "profile", "list", "record"],
262
293
  category: "devTools",
263
- redirectCompat: []
294
+ redirectCompat: ["atproto-explorer"]
264
295
  },
265
296
  bluepy: {
266
297
  id: "bluepy",
@@ -305,44 +336,44 @@ var WAYPOINT_DESTINATIONS_DATA = {
305
336
  redirectCompat: ["bluesky-social"],
306
337
  expectedCollections: ["app.bsky."]
307
338
  },
308
- catsky: {
309
- id: "catsky",
310
- name: "Catsky",
339
+ deer: {
340
+ id: "deer",
341
+ name: "Deer",
311
342
  description: (collection) => {
312
- if (collection === "app.bsky.feed.post") return "View post on catsky.social";
313
- if (collection === "app.bsky.graph.list") return "View list on catsky.social";
314
- return "View profile on catsky.social";
343
+ if (collection === "app.bsky.feed.post") return "View post on deer.social";
344
+ if (collection === "app.bsky.graph.list") return "View list on deer.social";
345
+ return "View profile on deer.social";
315
346
  },
316
347
  getUrl: (handle, collection, rkey) => {
317
348
  if (collection && rkey) {
318
349
  if (collection === "app.bsky.feed.post") {
319
- return `https://catsky.social/profile/${handle}/post/${rkey}`;
350
+ return `https://deer.social/profile/${handle}/post/${rkey}`;
320
351
  }
321
- return `https://catsky.social/profile/${handle}`;
352
+ return `https://deer.social/profile/${handle}`;
322
353
  }
323
- return `https://catsky.social/profile/${handle}`;
354
+ return `https://deer.social/profile/${handle}`;
324
355
  },
325
356
  supportedTypes: ["post", "profile", "list", "record"],
326
357
  category: "blueskyForks",
327
358
  redirectCompat: ["bluesky-social"],
328
359
  expectedCollections: ["app.bsky."]
329
360
  },
330
- deer: {
331
- id: "deer",
332
- name: "Deer",
361
+ mu: {
362
+ id: "mu",
363
+ name: "Mu",
333
364
  description: (collection) => {
334
- if (collection === "app.bsky.feed.post") return "View post on deer.social";
335
- if (collection === "app.bsky.graph.list") return "View list on deer.social";
336
- return "View profile on deer.social";
365
+ if (collection === "app.bsky.feed.post") return "View post on mu.social";
366
+ if (collection === "app.bsky.graph.list") return "View list on mu.social";
367
+ return "View profile on mu.social";
337
368
  },
338
369
  getUrl: (handle, collection, rkey) => {
339
370
  if (collection && rkey) {
340
371
  if (collection === "app.bsky.feed.post") {
341
- return `https://deer.social/profile/${handle}/post/${rkey}`;
372
+ return `https://mu.social/profile/${handle}/post/${rkey}`;
342
373
  }
343
- return `https://deer.social/profile/${handle}`;
374
+ return `https://mu.social/profile/${handle}`;
344
375
  }
345
- return `https://deer.social/profile/${handle}`;
376
+ return `https://mu.social/profile/${handle}`;
346
377
  },
347
378
  supportedTypes: ["post", "profile", "list", "record"],
348
379
  category: "blueskyForks",
@@ -555,14 +586,54 @@ var WAYPOINT_DESTINATIONS_DATA = {
555
586
  category: "publications",
556
587
  redirectCompat: ["standard-site"],
557
588
  expectedCollections: ["pub.leaflet.", "site.standard."]
589
+ },
590
+ standardReader: {
591
+ id: "standardReader",
592
+ name: "Standard Reader",
593
+ description: (collection) => {
594
+ if (collection?.startsWith("site.standard.") || collection?.startsWith("pub.leaflet.")) {
595
+ return "Read document on standard-reader.app";
596
+ }
597
+ return "View documents on standard-reader.app";
598
+ },
599
+ getUrl: (handle, collection, rkey, did) => {
600
+ const identifier = did || handle;
601
+ if (collection && rkey && (collection.startsWith("site.standard.") || collection.startsWith("pub.leaflet."))) {
602
+ return `https://standard-reader.app/a/${identifier}/${rkey}`;
603
+ }
604
+ return `https://standard-reader.app/u/${identifier}`;
605
+ },
606
+ supportedTypes: ["post", "profile", "list", "record"],
607
+ category: "publications",
608
+ redirectCompat: ["standard-site"],
609
+ expectedCollections: ["pub.leaflet.", "site.standard."]
610
+ },
611
+ taproot: {
612
+ id: "taproot",
613
+ name: "Taproot",
614
+ description: (collection) => {
615
+ if (collection) return "Inspect record on atproto.at";
616
+ return "Browse repo on atproto.at";
617
+ },
618
+ getUrl: (handle, collection, rkey, did) => {
619
+ const identifier = did || handle;
620
+ if (collection && rkey) {
621
+ return `https://atproto.at/uri/at://${identifier}/${collection}/${rkey}`;
622
+ }
623
+ return `https://atproto.at/uri/at://${identifier}`;
624
+ },
625
+ supportedTypes: ["post", "profile", "list", "record"],
626
+ category: "devTools",
627
+ redirectCompat: []
558
628
  }
559
629
  };
560
630
  var WAYPOINT_ORDER = [
561
631
  "anisota",
562
632
  "bluesky",
563
633
  "bluepy",
564
- "blacksky",
565
634
  "reddwarf",
635
+ "impro",
636
+ "blacksky",
566
637
  "leaflet",
567
638
  "aturi",
568
639
  "pinksky",
@@ -576,12 +647,14 @@ var WAYPOINT_ORDER = [
576
647
  "anisotaReader",
577
648
  "offprint",
578
649
  "pckt",
650
+ "standardReader",
579
651
  "aturiExplore",
580
652
  "pdsls",
581
653
  "tangled",
582
654
  "atptools",
655
+ "taproot",
583
656
  "witchsky",
584
- "catsky",
657
+ "mu",
585
658
  "deer"
586
659
  ];
587
660
  function getWaypointDataForType(type) {
@@ -676,13 +749,13 @@ var RECOMMENDED_WAYPOINTS = {
676
749
  label: "Recommended for Repos"
677
750
  },
678
751
  "record": {
679
- waypointIds: ["aturiExplore", "pdsls", "atptools"],
752
+ waypointIds: ["aturiExplore", "pdsls", "atptools", "taproot"],
680
753
  label: "Recommended for Records"
681
754
  }
682
755
  };
683
756
  var RECOMMENDED_NAMESPACE_PREFIXES = {
684
757
  "site.standard": {
685
- waypointIds: ["leaflet", "anisotaReader", "offprint", "pckt", "pdsls"],
758
+ waypointIds: ["leaflet", "standardReader", "anisotaReader", "offprint", "pckt", "pdsls"],
686
759
  label: "Recommended for Publications"
687
760
  },
688
761
  "pub.leaflet": {
@@ -750,6 +823,26 @@ function waypointActivity(waypoint, repoCollections) {
750
823
  return "absent";
751
824
  }
752
825
 
826
+ // src/upstreamFetch.ts
827
+ var RETRY_DELAY_MS = 250;
828
+ async function upstreamFetch(url, { timeoutMs = 8e3, retries = 1, ...init } = {}) {
829
+ let lastError;
830
+ for (let attempt = 0; attempt <= retries; attempt++) {
831
+ try {
832
+ return await fetch(url, {
833
+ ...init,
834
+ signal: AbortSignal.timeout(timeoutMs)
835
+ });
836
+ } catch (error) {
837
+ lastError = error;
838
+ if (attempt < retries) {
839
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
840
+ }
841
+ }
842
+ }
843
+ throw lastError;
844
+ }
845
+
753
846
  // src/uriParser.ts
754
847
  function parseURI(handle, collection, rkey) {
755
848
  if (!handle) {
@@ -791,25 +884,29 @@ function parseURI(handle, collection, rkey) {
791
884
  error: "Invalid URI structure"
792
885
  };
793
886
  }
794
- async function resolveHandle(handle) {
887
+ async function resolveHandleStatus(handle) {
795
888
  if (handle.startsWith("did:")) {
796
- return handle;
889
+ return { did: handle };
797
890
  }
798
891
  try {
799
892
  const apiUrl = process.env.NEXT_PUBLIC_BSKY_API_URL || "https://public.api.bsky.app";
800
- const response = await fetch(
893
+ const response = await upstreamFetch(
801
894
  `${apiUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`
802
895
  );
803
896
  if (!response.ok) {
804
- return null;
897
+ return { did: null, reason: response.status >= 500 ? "unavailable" : "not-found" };
805
898
  }
806
899
  const data = await response.json();
807
- return data.did || null;
900
+ if (data.did) return { did: data.did };
901
+ return { did: null, reason: "not-found" };
808
902
  } catch (error) {
809
903
  console.error("Error resolving handle:", error);
810
- return null;
904
+ return { did: null, reason: "unavailable" };
811
905
  }
812
906
  }
907
+ async function resolveHandle(handle) {
908
+ return (await resolveHandleStatus(handle)).did;
909
+ }
813
910
  function getDisplayName(handle, did) {
814
911
  if (handle.startsWith("did:")) {
815
912
  return did ? `@${did.slice(0, 16)}...` : "Unknown";
@@ -822,16 +919,28 @@ var BLUESKY_FAMILY = [
822
919
  { source: "bluesky", hosts: ["bsky.app"] },
823
920
  { source: "blacksky", hosts: ["blacksky.community"] },
824
921
  { source: "reddwarf", hosts: ["reddwarf.app"] },
922
+ { source: "impro", hosts: ["impro.social"] },
825
923
  { source: "witchsky", hosts: ["witchsky.app"] },
826
- { source: "catsky", hosts: ["catsky.social"] },
827
924
  { source: "deer", hosts: ["deer.social"] },
828
- { source: "anisota", hosts: ["anisota.net"] }
925
+ { source: "mu", hosts: ["mu.social"] },
926
+ { source: "anisota", hosts: ["anisota.net"], matchSubdomains: true }
829
927
  ];
928
+ var SUBDOMAIN_HOSTS = BLUESKY_FAMILY.filter((f) => f.matchSubdomains).flatMap(
929
+ (f) => f.hosts
930
+ );
830
931
  function normalizeHost(host) {
831
932
  return host.toLowerCase().replace(/^www\./, "");
832
933
  }
934
+ function isSubdomainOf(host, base) {
935
+ return host.endsWith(`.${base}`);
936
+ }
937
+ function hostMatchesConfig(host, config) {
938
+ if (config.hosts.includes(host)) return true;
939
+ if (!config.matchSubdomains) return false;
940
+ return config.hosts.some((h) => isSubdomainOf(host, h));
941
+ }
833
942
  function matchBlueskyFamily(host, parts) {
834
- const entry = BLUESKY_FAMILY.find((f) => f.hosts.includes(host));
943
+ const entry = BLUESKY_FAMILY.find((f) => hostMatchesConfig(host, f));
835
944
  if (!entry) return null;
836
945
  if (parts[0] !== "profile" || !parts[1]) return null;
837
946
  const handle = parts[1];
@@ -873,6 +982,19 @@ function matchBlueskyFamily(host, parts) {
873
982
  }
874
983
  };
875
984
  }
985
+ if (parts[2] === "document" && parts[3]) {
986
+ return {
987
+ source: entry.source,
988
+ parsed: {
989
+ type: "record",
990
+ uri: `at://${handle}/site.standard.document/${parts[3]}`,
991
+ handle,
992
+ did,
993
+ collection: "site.standard.document",
994
+ rkey: parts[3]
995
+ }
996
+ };
997
+ }
876
998
  return {
877
999
  source: entry.source,
878
1000
  parsed: {
@@ -1143,11 +1265,160 @@ function matchBlento(host, parts) {
1143
1265
  parsed: { type: "profile", uri: `at://${handle}`, handle, did }
1144
1266
  };
1145
1267
  }
1268
+ function matchStandardReader(host, parts) {
1269
+ if (host !== "standard-reader.app") return null;
1270
+ if (parts[0] === "a" && parts[1] && parts[2]) {
1271
+ const handle = parts[1];
1272
+ const did = handle.startsWith("did:") ? handle : void 0;
1273
+ const rkey = parts[2];
1274
+ return {
1275
+ source: "standardReader",
1276
+ parsed: {
1277
+ type: "record",
1278
+ uri: `at://${handle}/site.standard.document/${rkey}`,
1279
+ handle,
1280
+ did,
1281
+ collection: "site.standard.document",
1282
+ rkey
1283
+ }
1284
+ };
1285
+ }
1286
+ if (parts[0] === "u" && parts[1]) {
1287
+ const handle = parts[1];
1288
+ const did = handle.startsWith("did:") ? handle : void 0;
1289
+ return {
1290
+ source: "standardReader",
1291
+ parsed: { type: "profile", uri: `at://${handle}`, handle, did }
1292
+ };
1293
+ }
1294
+ return null;
1295
+ }
1296
+ function matchFlatRecordHost(host, parts, target) {
1297
+ if (host !== target.host) return null;
1298
+ const [handle, collection, rkey] = parts;
1299
+ if (!handle || !collection || !rkey) return null;
1300
+ if (!collection.includes(".")) return null;
1301
+ const did = handle.startsWith("did:") ? handle : void 0;
1302
+ return {
1303
+ source: target.source,
1304
+ parsed: {
1305
+ type: inferType(collection),
1306
+ uri: `at://${handle}/${collection}/${rkey}`,
1307
+ handle,
1308
+ did,
1309
+ collection,
1310
+ rkey
1311
+ }
1312
+ };
1313
+ }
1314
+ function matchTaproot(host, pathname) {
1315
+ if (host !== "atproto.at") return null;
1316
+ const m = pathname.replace(/^\/+/, "").match(/^uri\/at:\/{1,2}(.+)$/);
1317
+ if (!m) return null;
1318
+ const segments = m[1].split("/").filter(Boolean);
1319
+ if (segments.length === 0) return null;
1320
+ const handle = segments[0];
1321
+ const collection = segments[1];
1322
+ const rkey = segments[2];
1323
+ const did = handle.startsWith("did:") ? handle : void 0;
1324
+ if (collection && rkey) {
1325
+ return {
1326
+ source: "taproot",
1327
+ parsed: {
1328
+ type: inferType(collection),
1329
+ uri: `at://${handle}/${collection}/${rkey}`,
1330
+ handle,
1331
+ did,
1332
+ collection,
1333
+ rkey
1334
+ }
1335
+ };
1336
+ }
1337
+ return {
1338
+ source: "taproot",
1339
+ parsed: { type: "profile", uri: `at://${handle}`, handle, did }
1340
+ };
1341
+ }
1342
+ function matchAturi(host, parts) {
1343
+ if (host !== "aturi.to") return null;
1344
+ const isIdentifier = (seg) => !!seg && (seg.startsWith("did:") || seg.includes("."));
1345
+ if (parts[0] === "explore" && isIdentifier(parts[1])) {
1346
+ const handle = parts[1];
1347
+ const did = handle.startsWith("did:") ? handle : void 0;
1348
+ const collection = parts[2];
1349
+ const rkey = parts[3];
1350
+ if (collection && rkey) {
1351
+ return {
1352
+ source: "aturiExplore",
1353
+ parsed: {
1354
+ type: inferType(collection),
1355
+ uri: `at://${handle}/${collection}/${rkey}`,
1356
+ handle,
1357
+ did,
1358
+ collection,
1359
+ rkey
1360
+ }
1361
+ };
1362
+ }
1363
+ return {
1364
+ source: "aturiExplore",
1365
+ parsed: { type: "profile", uri: `at://${handle}`, handle, did }
1366
+ };
1367
+ }
1368
+ if (parts[0] === "profile" && isIdentifier(parts[1])) {
1369
+ const handle = parts[1];
1370
+ const did = handle.startsWith("did:") ? handle : void 0;
1371
+ if (parts[2] === "post" && parts[3]) {
1372
+ return {
1373
+ source: "aturi",
1374
+ parsed: {
1375
+ type: "post",
1376
+ uri: `at://${handle}/app.bsky.feed.post/${parts[3]}`,
1377
+ handle,
1378
+ did,
1379
+ collection: "app.bsky.feed.post",
1380
+ rkey: parts[3]
1381
+ }
1382
+ };
1383
+ }
1384
+ if (parts[2] === "lists" && parts[3]) {
1385
+ return {
1386
+ source: "aturi",
1387
+ parsed: {
1388
+ type: "list",
1389
+ uri: `at://${handle}/app.bsky.graph.list/${parts[3]}`,
1390
+ handle,
1391
+ did,
1392
+ collection: "app.bsky.graph.list",
1393
+ rkey: parts[3]
1394
+ }
1395
+ };
1396
+ }
1397
+ if (parts[2] && parts[2].includes(".") && parts[3]) {
1398
+ return {
1399
+ source: "aturi",
1400
+ parsed: {
1401
+ type: inferType(parts[2]),
1402
+ uri: `at://${handle}/${parts[2]}/${parts[3]}`,
1403
+ handle,
1404
+ did,
1405
+ collection: parts[2],
1406
+ rkey: parts[3]
1407
+ }
1408
+ };
1409
+ }
1410
+ return {
1411
+ source: "aturi",
1412
+ parsed: { type: "profile", uri: `at://${handle}`, handle, did }
1413
+ };
1414
+ }
1415
+ return null;
1416
+ }
1146
1417
  function matchSupportedUrl(url) {
1147
1418
  const host = normalizeHost(url.hostname);
1148
1419
  const parts = url.pathname.split("/").filter(Boolean);
1149
1420
  const search = url.searchParams;
1150
- return matchBlueskyFamily(host, parts) || matchPinksky(host, parts, search) || matchLeaflet(host, parts) || matchTangled(host, parts) || matchMargin(host, parts) || matchPdsls(host, url.pathname) || matchAtpTools(host, url.pathname) || matchBluepy(host, url.pathname) || matchSemble(host, parts) || matchStreamplace(host, parts) || matchGrain(host, parts) || matchPopfeed(host, parts) || matchSifa(host, parts) || matchBlento(host, parts);
1421
+ return matchAturi(host, parts) || matchBlueskyFamily(host, parts) || matchPinksky(host, parts, search) || matchLeaflet(host, parts) || matchTangled(host, parts) || matchMargin(host, parts) || matchPdsls(host, url.pathname) || matchAtpTools(host, url.pathname) || matchBluepy(host, url.pathname) || matchSemble(host, parts) || matchStreamplace(host, parts) || matchGrain(host, parts) || matchPopfeed(host, parts) || matchSifa(host, parts) || matchBlento(host, parts) || matchStandardReader(host, parts) || matchFlatRecordHost(host, parts, { host: "offprint.app", source: "offprint" }) || matchFlatRecordHost(host, parts, { host: "pckt.blog", source: "pckt" }) || matchTaproot(host, url.pathname);
1151
1422
  }
1152
1423
  function parseAtUri(uri) {
1153
1424
  if (!uri.startsWith("at://")) return null;
@@ -1177,6 +1448,7 @@ function parseAtUri(uri) {
1177
1448
  };
1178
1449
  }
1179
1450
  var SUPPORTED_HOSTS = [
1451
+ "aturi.to",
1180
1452
  ...BLUESKY_FAMILY.flatMap((e) => e.hosts),
1181
1453
  "pinkleap.app",
1182
1454
  "leaflet.pub",
@@ -1190,8 +1462,17 @@ var SUPPORTED_HOSTS = [
1190
1462
  "grain.social",
1191
1463
  "popfeed.social",
1192
1464
  "sifa.id",
1193
- "blento.app"
1465
+ "blento.app",
1466
+ "standard-reader.app",
1467
+ "offprint.app",
1468
+ "pckt.blog",
1469
+ "atproto.at"
1194
1470
  ];
1471
+ function isSupportedHost(host) {
1472
+ const h = normalizeHost(host);
1473
+ if (SUPPORTED_HOSTS.includes(h)) return true;
1474
+ return SUBDOMAIN_HOSTS.some((base) => isSubdomainOf(h, base));
1475
+ }
1195
1476
 
1196
1477
  // src/resolve.ts
1197
1478
  var DID_REQUIRED_WAYPOINTS = /* @__PURE__ */ new Set([
@@ -1339,11 +1620,13 @@ exports.getFeaturedWaypointData = getFeaturedWaypointData;
1339
1620
  exports.getRecommendedWaypointsData = getRecommendedWaypointsData;
1340
1621
  exports.getWaypointCountData = getWaypointCountData;
1341
1622
  exports.getWaypointDataForType = getWaypointDataForType;
1623
+ exports.isSupportedHost = isSupportedHost;
1342
1624
  exports.matchSupportedUrl = matchSupportedUrl;
1343
1625
  exports.parseAtUri = parseAtUri;
1344
1626
  exports.parseURI = parseURI;
1345
1627
  exports.resolveAtUri = resolveAtUri;
1346
1628
  exports.resolveHandle = resolveHandle;
1629
+ exports.resolveHandleStatus = resolveHandleStatus;
1347
1630
  exports.resolveUrl = resolveUrl;
1348
1631
  exports.resolveViaApi = resolveViaApi;
1349
1632
  exports.waypointActivity = waypointActivity;