@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.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
|
-
|
|
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
|
-
|
|
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 = (
|
|
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 {
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|