@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/README.md +253 -5
- package/dist/icons.cjs +106 -0
- package/dist/icons.cjs.map +1 -0
- package/dist/icons.d.cts +80 -0
- package/dist/icons.d.ts +80 -0
- package/dist/icons.js +73 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.cjs +411 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +265 -4
- package/dist/index.d.ts +265 -4
- package/dist/index.js +398 -10
- package/dist/index.js.map +1 -1
- package/package.json +22 -3
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
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 {
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|