@hasna/shortlinks 0.1.16 → 0.1.18
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/cli/index.js +124 -10
- package/dist/index.js +124 -10
- package/dist/server.js +124 -10
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -9702,6 +9702,110 @@ function json(data, status = 200) {
|
|
|
9702
9702
|
headers: { "content-type": "application/json; charset=utf-8" }
|
|
9703
9703
|
});
|
|
9704
9704
|
}
|
|
9705
|
+
function htmlEscape(value) {
|
|
9706
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
9707
|
+
}
|
|
9708
|
+
function publicErrorPage(input) {
|
|
9709
|
+
const detail = input.detail ? `<p class="detail">${htmlEscape(input.detail)}</p>` : "";
|
|
9710
|
+
const slug = input.slug ? `<p class="slug">${htmlEscape(input.slug)}</p>` : "";
|
|
9711
|
+
const body = `<!doctype html>
|
|
9712
|
+
<html lang="en">
|
|
9713
|
+
<head>
|
|
9714
|
+
<meta charset="utf-8">
|
|
9715
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
9716
|
+
<title>${htmlEscape(input.title)} - Shortlink</title>
|
|
9717
|
+
<style>
|
|
9718
|
+
:root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
9719
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f5f7f8; color: #172026; }
|
|
9720
|
+
main { width: min(92vw, 520px); border: 1px solid #d7dee3; border-radius: 8px; background: #fff; padding: 28px; box-shadow: 0 18px 48px rgb(23 32 38 / 10%); }
|
|
9721
|
+
.status { display: inline-flex; align-items: center; min-height: 28px; padding: 0 10px; border-radius: 999px; background: #eef2f5; color: #46545f; font-size: 13px; font-weight: 700; }
|
|
9722
|
+
h1 { margin: 18px 0 10px; font-size: 24px; line-height: 1.2; letter-spacing: 0; }
|
|
9723
|
+
p { margin: 0; color: #46545f; line-height: 1.55; }
|
|
9724
|
+
.detail { margin-top: 12px; color: #6a7780; font-size: 14px; }
|
|
9725
|
+
.slug { margin-top: 18px; padding: 10px 12px; border: 1px solid #d7dee3; border-radius: 6px; background: #fafbfc; color: #172026; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; overflow-wrap: anywhere; }
|
|
9726
|
+
@media (prefers-color-scheme: dark) {
|
|
9727
|
+
body { background: #101417; color: #f4f7f8; }
|
|
9728
|
+
main { background: #171d21; border-color: #2b363d; box-shadow: none; }
|
|
9729
|
+
.status { background: #263139; color: #cbd5db; }
|
|
9730
|
+
p { color: #bac6cc; }
|
|
9731
|
+
.detail { color: #8c9aa3; }
|
|
9732
|
+
.slug { background: #101417; border-color: #2b363d; color: #f4f7f8; }
|
|
9733
|
+
}
|
|
9734
|
+
</style>
|
|
9735
|
+
</head>
|
|
9736
|
+
<body>
|
|
9737
|
+
<main>
|
|
9738
|
+
<div class="status">${input.status}</div>
|
|
9739
|
+
<h1>${htmlEscape(input.title)}</h1>
|
|
9740
|
+
<p>${htmlEscape(input.message)}</p>
|
|
9741
|
+
${detail}
|
|
9742
|
+
${slug}
|
|
9743
|
+
</main>
|
|
9744
|
+
</body>
|
|
9745
|
+
</html>`;
|
|
9746
|
+
return new Response(body, {
|
|
9747
|
+
status: input.status,
|
|
9748
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }
|
|
9749
|
+
});
|
|
9750
|
+
}
|
|
9751
|
+
function publicShortlinkError(kind, slug, host) {
|
|
9752
|
+
if (kind === "disabled") {
|
|
9753
|
+
return publicErrorPage({
|
|
9754
|
+
title: "This shortlink is disabled",
|
|
9755
|
+
message: "The owner has turned this shortlink off.",
|
|
9756
|
+
detail: "Ask the sender for a new link if you still need access.",
|
|
9757
|
+
status: 410,
|
|
9758
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
9759
|
+
});
|
|
9760
|
+
}
|
|
9761
|
+
if (kind === "expired") {
|
|
9762
|
+
return publicErrorPage({
|
|
9763
|
+
title: "This shortlink has expired",
|
|
9764
|
+
message: "The owner set an expiration time for this shortlink, and it is no longer available.",
|
|
9765
|
+
detail: "Ask the sender to create a fresh link.",
|
|
9766
|
+
status: 410,
|
|
9767
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
9768
|
+
});
|
|
9769
|
+
}
|
|
9770
|
+
if (kind === "used") {
|
|
9771
|
+
return publicErrorPage({
|
|
9772
|
+
title: "This shortlink has already been used",
|
|
9773
|
+
message: "The owner limited how many times this shortlink can be opened, and that limit has been reached.",
|
|
9774
|
+
detail: "Ask the sender for a new link if you still need access.",
|
|
9775
|
+
status: 410,
|
|
9776
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
9777
|
+
});
|
|
9778
|
+
}
|
|
9779
|
+
if (kind === "reserved") {
|
|
9780
|
+
return publicErrorPage({
|
|
9781
|
+
title: "This path is reserved",
|
|
9782
|
+
message: "This address is reserved for another has.na feature.",
|
|
9783
|
+
status: 404,
|
|
9784
|
+
slug
|
|
9785
|
+
});
|
|
9786
|
+
}
|
|
9787
|
+
if (kind === "invalid") {
|
|
9788
|
+
return publicErrorPage({
|
|
9789
|
+
title: "Invalid shortlink",
|
|
9790
|
+
message: "This shortlink address is not valid.",
|
|
9791
|
+
status: 400
|
|
9792
|
+
});
|
|
9793
|
+
}
|
|
9794
|
+
if (kind === "missing") {
|
|
9795
|
+
return publicErrorPage({
|
|
9796
|
+
title: "Missing shortlink",
|
|
9797
|
+
message: "No shortlink slug was provided.",
|
|
9798
|
+
status: 404
|
|
9799
|
+
});
|
|
9800
|
+
}
|
|
9801
|
+
return publicErrorPage({
|
|
9802
|
+
title: "Shortlink not found",
|
|
9803
|
+
message: "This shortlink does not exist or is no longer available.",
|
|
9804
|
+
detail: "Check the address or ask the sender for a new link.",
|
|
9805
|
+
status: 404,
|
|
9806
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
9807
|
+
});
|
|
9808
|
+
}
|
|
9705
9809
|
function apiToken(options) {
|
|
9706
9810
|
return options.apiToken || process.env.SHORTLINKS_API_TOKEN || process.env.HASNA_SHORTLINKS_API_TOKEN || null;
|
|
9707
9811
|
}
|
|
@@ -9852,34 +9956,44 @@ function createShortlinksHandler(options = {}) {
|
|
|
9852
9956
|
try {
|
|
9853
9957
|
slug = decodeURIComponent(url.pathname.replace(/^\/+/, "").split("/")[0] || "");
|
|
9854
9958
|
} catch {
|
|
9855
|
-
return
|
|
9959
|
+
return publicShortlinkError("invalid", "");
|
|
9856
9960
|
}
|
|
9857
9961
|
if (!slug)
|
|
9858
|
-
return
|
|
9962
|
+
return publicShortlinkError("missing", "");
|
|
9859
9963
|
if (reservedPathPrefixes.has(slug.toLowerCase())) {
|
|
9860
|
-
return
|
|
9964
|
+
return publicShortlinkError("reserved", slug);
|
|
9861
9965
|
}
|
|
9862
9966
|
const host = getHost(request, options.defaultHost);
|
|
9863
9967
|
if (!host)
|
|
9864
|
-
return
|
|
9968
|
+
return publicErrorPage({
|
|
9969
|
+
title: "Invalid shortlink request",
|
|
9970
|
+
message: "This request did not include a host name.",
|
|
9971
|
+
status: 400
|
|
9972
|
+
});
|
|
9865
9973
|
let link = null;
|
|
9866
9974
|
try {
|
|
9867
9975
|
link = await store.resolve(host, slug);
|
|
9868
9976
|
} catch {
|
|
9869
|
-
return
|
|
9977
|
+
return publicShortlinkError("not_found", slug, host);
|
|
9870
9978
|
}
|
|
9871
9979
|
if (!link)
|
|
9872
|
-
return
|
|
9980
|
+
return publicShortlinkError("not_found", slug, host);
|
|
9873
9981
|
if (!link.active)
|
|
9874
|
-
return
|
|
9982
|
+
return publicShortlinkError("disabled", slug, host);
|
|
9875
9983
|
if (isExpired(link))
|
|
9876
|
-
return
|
|
9984
|
+
return publicShortlinkError("expired", slug, host);
|
|
9877
9985
|
if (link.max_uses !== null && link.used_count >= link.max_uses) {
|
|
9878
|
-
return
|
|
9986
|
+
return publicShortlinkError("used", slug, host);
|
|
9987
|
+
}
|
|
9988
|
+
if (request.method.toUpperCase() === "HEAD") {
|
|
9989
|
+
return new Response(null, {
|
|
9990
|
+
status: redirectStatus,
|
|
9991
|
+
headers: { location: link.destination_url }
|
|
9992
|
+
});
|
|
9879
9993
|
}
|
|
9880
9994
|
const consumed = store.consumeLinkUse ? await store.consumeLinkUse(link) : link;
|
|
9881
9995
|
if (!consumed) {
|
|
9882
|
-
return
|
|
9996
|
+
return publicShortlinkError("used", slug, host);
|
|
9883
9997
|
}
|
|
9884
9998
|
await store.recordClick(consumed, {
|
|
9885
9999
|
ip: getClientIp(request),
|
package/dist/index.js
CHANGED
|
@@ -6283,6 +6283,110 @@ function json(data, status = 200) {
|
|
|
6283
6283
|
headers: { "content-type": "application/json; charset=utf-8" }
|
|
6284
6284
|
});
|
|
6285
6285
|
}
|
|
6286
|
+
function htmlEscape(value) {
|
|
6287
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6288
|
+
}
|
|
6289
|
+
function publicErrorPage(input) {
|
|
6290
|
+
const detail = input.detail ? `<p class="detail">${htmlEscape(input.detail)}</p>` : "";
|
|
6291
|
+
const slug = input.slug ? `<p class="slug">${htmlEscape(input.slug)}</p>` : "";
|
|
6292
|
+
const body = `<!doctype html>
|
|
6293
|
+
<html lang="en">
|
|
6294
|
+
<head>
|
|
6295
|
+
<meta charset="utf-8">
|
|
6296
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6297
|
+
<title>${htmlEscape(input.title)} - Shortlink</title>
|
|
6298
|
+
<style>
|
|
6299
|
+
:root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
6300
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f5f7f8; color: #172026; }
|
|
6301
|
+
main { width: min(92vw, 520px); border: 1px solid #d7dee3; border-radius: 8px; background: #fff; padding: 28px; box-shadow: 0 18px 48px rgb(23 32 38 / 10%); }
|
|
6302
|
+
.status { display: inline-flex; align-items: center; min-height: 28px; padding: 0 10px; border-radius: 999px; background: #eef2f5; color: #46545f; font-size: 13px; font-weight: 700; }
|
|
6303
|
+
h1 { margin: 18px 0 10px; font-size: 24px; line-height: 1.2; letter-spacing: 0; }
|
|
6304
|
+
p { margin: 0; color: #46545f; line-height: 1.55; }
|
|
6305
|
+
.detail { margin-top: 12px; color: #6a7780; font-size: 14px; }
|
|
6306
|
+
.slug { margin-top: 18px; padding: 10px 12px; border: 1px solid #d7dee3; border-radius: 6px; background: #fafbfc; color: #172026; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; overflow-wrap: anywhere; }
|
|
6307
|
+
@media (prefers-color-scheme: dark) {
|
|
6308
|
+
body { background: #101417; color: #f4f7f8; }
|
|
6309
|
+
main { background: #171d21; border-color: #2b363d; box-shadow: none; }
|
|
6310
|
+
.status { background: #263139; color: #cbd5db; }
|
|
6311
|
+
p { color: #bac6cc; }
|
|
6312
|
+
.detail { color: #8c9aa3; }
|
|
6313
|
+
.slug { background: #101417; border-color: #2b363d; color: #f4f7f8; }
|
|
6314
|
+
}
|
|
6315
|
+
</style>
|
|
6316
|
+
</head>
|
|
6317
|
+
<body>
|
|
6318
|
+
<main>
|
|
6319
|
+
<div class="status">${input.status}</div>
|
|
6320
|
+
<h1>${htmlEscape(input.title)}</h1>
|
|
6321
|
+
<p>${htmlEscape(input.message)}</p>
|
|
6322
|
+
${detail}
|
|
6323
|
+
${slug}
|
|
6324
|
+
</main>
|
|
6325
|
+
</body>
|
|
6326
|
+
</html>`;
|
|
6327
|
+
return new Response(body, {
|
|
6328
|
+
status: input.status,
|
|
6329
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }
|
|
6330
|
+
});
|
|
6331
|
+
}
|
|
6332
|
+
function publicShortlinkError(kind, slug, host) {
|
|
6333
|
+
if (kind === "disabled") {
|
|
6334
|
+
return publicErrorPage({
|
|
6335
|
+
title: "This shortlink is disabled",
|
|
6336
|
+
message: "The owner has turned this shortlink off.",
|
|
6337
|
+
detail: "Ask the sender for a new link if you still need access.",
|
|
6338
|
+
status: 410,
|
|
6339
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
6340
|
+
});
|
|
6341
|
+
}
|
|
6342
|
+
if (kind === "expired") {
|
|
6343
|
+
return publicErrorPage({
|
|
6344
|
+
title: "This shortlink has expired",
|
|
6345
|
+
message: "The owner set an expiration time for this shortlink, and it is no longer available.",
|
|
6346
|
+
detail: "Ask the sender to create a fresh link.",
|
|
6347
|
+
status: 410,
|
|
6348
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
6349
|
+
});
|
|
6350
|
+
}
|
|
6351
|
+
if (kind === "used") {
|
|
6352
|
+
return publicErrorPage({
|
|
6353
|
+
title: "This shortlink has already been used",
|
|
6354
|
+
message: "The owner limited how many times this shortlink can be opened, and that limit has been reached.",
|
|
6355
|
+
detail: "Ask the sender for a new link if you still need access.",
|
|
6356
|
+
status: 410,
|
|
6357
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
6358
|
+
});
|
|
6359
|
+
}
|
|
6360
|
+
if (kind === "reserved") {
|
|
6361
|
+
return publicErrorPage({
|
|
6362
|
+
title: "This path is reserved",
|
|
6363
|
+
message: "This address is reserved for another has.na feature.",
|
|
6364
|
+
status: 404,
|
|
6365
|
+
slug
|
|
6366
|
+
});
|
|
6367
|
+
}
|
|
6368
|
+
if (kind === "invalid") {
|
|
6369
|
+
return publicErrorPage({
|
|
6370
|
+
title: "Invalid shortlink",
|
|
6371
|
+
message: "This shortlink address is not valid.",
|
|
6372
|
+
status: 400
|
|
6373
|
+
});
|
|
6374
|
+
}
|
|
6375
|
+
if (kind === "missing") {
|
|
6376
|
+
return publicErrorPage({
|
|
6377
|
+
title: "Missing shortlink",
|
|
6378
|
+
message: "No shortlink slug was provided.",
|
|
6379
|
+
status: 404
|
|
6380
|
+
});
|
|
6381
|
+
}
|
|
6382
|
+
return publicErrorPage({
|
|
6383
|
+
title: "Shortlink not found",
|
|
6384
|
+
message: "This shortlink does not exist or is no longer available.",
|
|
6385
|
+
detail: "Check the address or ask the sender for a new link.",
|
|
6386
|
+
status: 404,
|
|
6387
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
6388
|
+
});
|
|
6389
|
+
}
|
|
6286
6390
|
function apiToken(options) {
|
|
6287
6391
|
return options.apiToken || process.env.SHORTLINKS_API_TOKEN || process.env.HASNA_SHORTLINKS_API_TOKEN || null;
|
|
6288
6392
|
}
|
|
@@ -6433,34 +6537,44 @@ function createShortlinksHandler(options = {}) {
|
|
|
6433
6537
|
try {
|
|
6434
6538
|
slug = decodeURIComponent(url.pathname.replace(/^\/+/, "").split("/")[0] || "");
|
|
6435
6539
|
} catch {
|
|
6436
|
-
return
|
|
6540
|
+
return publicShortlinkError("invalid", "");
|
|
6437
6541
|
}
|
|
6438
6542
|
if (!slug)
|
|
6439
|
-
return
|
|
6543
|
+
return publicShortlinkError("missing", "");
|
|
6440
6544
|
if (reservedPathPrefixes.has(slug.toLowerCase())) {
|
|
6441
|
-
return
|
|
6545
|
+
return publicShortlinkError("reserved", slug);
|
|
6442
6546
|
}
|
|
6443
6547
|
const host = getHost(request, options.defaultHost);
|
|
6444
6548
|
if (!host)
|
|
6445
|
-
return
|
|
6549
|
+
return publicErrorPage({
|
|
6550
|
+
title: "Invalid shortlink request",
|
|
6551
|
+
message: "This request did not include a host name.",
|
|
6552
|
+
status: 400
|
|
6553
|
+
});
|
|
6446
6554
|
let link = null;
|
|
6447
6555
|
try {
|
|
6448
6556
|
link = await store.resolve(host, slug);
|
|
6449
6557
|
} catch {
|
|
6450
|
-
return
|
|
6558
|
+
return publicShortlinkError("not_found", slug, host);
|
|
6451
6559
|
}
|
|
6452
6560
|
if (!link)
|
|
6453
|
-
return
|
|
6561
|
+
return publicShortlinkError("not_found", slug, host);
|
|
6454
6562
|
if (!link.active)
|
|
6455
|
-
return
|
|
6563
|
+
return publicShortlinkError("disabled", slug, host);
|
|
6456
6564
|
if (isExpired(link))
|
|
6457
|
-
return
|
|
6565
|
+
return publicShortlinkError("expired", slug, host);
|
|
6458
6566
|
if (link.max_uses !== null && link.used_count >= link.max_uses) {
|
|
6459
|
-
return
|
|
6567
|
+
return publicShortlinkError("used", slug, host);
|
|
6568
|
+
}
|
|
6569
|
+
if (request.method.toUpperCase() === "HEAD") {
|
|
6570
|
+
return new Response(null, {
|
|
6571
|
+
status: redirectStatus,
|
|
6572
|
+
headers: { location: link.destination_url }
|
|
6573
|
+
});
|
|
6460
6574
|
}
|
|
6461
6575
|
const consumed = store.consumeLinkUse ? await store.consumeLinkUse(link) : link;
|
|
6462
6576
|
if (!consumed) {
|
|
6463
|
-
return
|
|
6577
|
+
return publicShortlinkError("used", slug, host);
|
|
6464
6578
|
}
|
|
6465
6579
|
await store.recordClick(consumed, {
|
|
6466
6580
|
ip: getClientIp(request),
|
package/dist/server.js
CHANGED
|
@@ -570,6 +570,110 @@ function json(data, status = 200) {
|
|
|
570
570
|
headers: { "content-type": "application/json; charset=utf-8" }
|
|
571
571
|
});
|
|
572
572
|
}
|
|
573
|
+
function htmlEscape(value) {
|
|
574
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
575
|
+
}
|
|
576
|
+
function publicErrorPage(input) {
|
|
577
|
+
const detail = input.detail ? `<p class="detail">${htmlEscape(input.detail)}</p>` : "";
|
|
578
|
+
const slug = input.slug ? `<p class="slug">${htmlEscape(input.slug)}</p>` : "";
|
|
579
|
+
const body = `<!doctype html>
|
|
580
|
+
<html lang="en">
|
|
581
|
+
<head>
|
|
582
|
+
<meta charset="utf-8">
|
|
583
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
584
|
+
<title>${htmlEscape(input.title)} - Shortlink</title>
|
|
585
|
+
<style>
|
|
586
|
+
:root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
587
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f5f7f8; color: #172026; }
|
|
588
|
+
main { width: min(92vw, 520px); border: 1px solid #d7dee3; border-radius: 8px; background: #fff; padding: 28px; box-shadow: 0 18px 48px rgb(23 32 38 / 10%); }
|
|
589
|
+
.status { display: inline-flex; align-items: center; min-height: 28px; padding: 0 10px; border-radius: 999px; background: #eef2f5; color: #46545f; font-size: 13px; font-weight: 700; }
|
|
590
|
+
h1 { margin: 18px 0 10px; font-size: 24px; line-height: 1.2; letter-spacing: 0; }
|
|
591
|
+
p { margin: 0; color: #46545f; line-height: 1.55; }
|
|
592
|
+
.detail { margin-top: 12px; color: #6a7780; font-size: 14px; }
|
|
593
|
+
.slug { margin-top: 18px; padding: 10px 12px; border: 1px solid #d7dee3; border-radius: 6px; background: #fafbfc; color: #172026; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; overflow-wrap: anywhere; }
|
|
594
|
+
@media (prefers-color-scheme: dark) {
|
|
595
|
+
body { background: #101417; color: #f4f7f8; }
|
|
596
|
+
main { background: #171d21; border-color: #2b363d; box-shadow: none; }
|
|
597
|
+
.status { background: #263139; color: #cbd5db; }
|
|
598
|
+
p { color: #bac6cc; }
|
|
599
|
+
.detail { color: #8c9aa3; }
|
|
600
|
+
.slug { background: #101417; border-color: #2b363d; color: #f4f7f8; }
|
|
601
|
+
}
|
|
602
|
+
</style>
|
|
603
|
+
</head>
|
|
604
|
+
<body>
|
|
605
|
+
<main>
|
|
606
|
+
<div class="status">${input.status}</div>
|
|
607
|
+
<h1>${htmlEscape(input.title)}</h1>
|
|
608
|
+
<p>${htmlEscape(input.message)}</p>
|
|
609
|
+
${detail}
|
|
610
|
+
${slug}
|
|
611
|
+
</main>
|
|
612
|
+
</body>
|
|
613
|
+
</html>`;
|
|
614
|
+
return new Response(body, {
|
|
615
|
+
status: input.status,
|
|
616
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
function publicShortlinkError(kind, slug, host) {
|
|
620
|
+
if (kind === "disabled") {
|
|
621
|
+
return publicErrorPage({
|
|
622
|
+
title: "This shortlink is disabled",
|
|
623
|
+
message: "The owner has turned this shortlink off.",
|
|
624
|
+
detail: "Ask the sender for a new link if you still need access.",
|
|
625
|
+
status: 410,
|
|
626
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
if (kind === "expired") {
|
|
630
|
+
return publicErrorPage({
|
|
631
|
+
title: "This shortlink has expired",
|
|
632
|
+
message: "The owner set an expiration time for this shortlink, and it is no longer available.",
|
|
633
|
+
detail: "Ask the sender to create a fresh link.",
|
|
634
|
+
status: 410,
|
|
635
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
if (kind === "used") {
|
|
639
|
+
return publicErrorPage({
|
|
640
|
+
title: "This shortlink has already been used",
|
|
641
|
+
message: "The owner limited how many times this shortlink can be opened, and that limit has been reached.",
|
|
642
|
+
detail: "Ask the sender for a new link if you still need access.",
|
|
643
|
+
status: 410,
|
|
644
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
if (kind === "reserved") {
|
|
648
|
+
return publicErrorPage({
|
|
649
|
+
title: "This path is reserved",
|
|
650
|
+
message: "This address is reserved for another has.na feature.",
|
|
651
|
+
status: 404,
|
|
652
|
+
slug
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (kind === "invalid") {
|
|
656
|
+
return publicErrorPage({
|
|
657
|
+
title: "Invalid shortlink",
|
|
658
|
+
message: "This shortlink address is not valid.",
|
|
659
|
+
status: 400
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
if (kind === "missing") {
|
|
663
|
+
return publicErrorPage({
|
|
664
|
+
title: "Missing shortlink",
|
|
665
|
+
message: "No shortlink slug was provided.",
|
|
666
|
+
status: 404
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
return publicErrorPage({
|
|
670
|
+
title: "Shortlink not found",
|
|
671
|
+
message: "This shortlink does not exist or is no longer available.",
|
|
672
|
+
detail: "Check the address or ask the sender for a new link.",
|
|
673
|
+
status: 404,
|
|
674
|
+
slug: slug ? `${host ?? ""}/${slug}` : undefined
|
|
675
|
+
});
|
|
676
|
+
}
|
|
573
677
|
function apiToken(options) {
|
|
574
678
|
return options.apiToken || process.env.SHORTLINKS_API_TOKEN || process.env.HASNA_SHORTLINKS_API_TOKEN || null;
|
|
575
679
|
}
|
|
@@ -720,34 +824,44 @@ function createShortlinksHandler(options = {}) {
|
|
|
720
824
|
try {
|
|
721
825
|
slug = decodeURIComponent(url.pathname.replace(/^\/+/, "").split("/")[0] || "");
|
|
722
826
|
} catch {
|
|
723
|
-
return
|
|
827
|
+
return publicShortlinkError("invalid", "");
|
|
724
828
|
}
|
|
725
829
|
if (!slug)
|
|
726
|
-
return
|
|
830
|
+
return publicShortlinkError("missing", "");
|
|
727
831
|
if (reservedPathPrefixes.has(slug.toLowerCase())) {
|
|
728
|
-
return
|
|
832
|
+
return publicShortlinkError("reserved", slug);
|
|
729
833
|
}
|
|
730
834
|
const host = getHost(request, options.defaultHost);
|
|
731
835
|
if (!host)
|
|
732
|
-
return
|
|
836
|
+
return publicErrorPage({
|
|
837
|
+
title: "Invalid shortlink request",
|
|
838
|
+
message: "This request did not include a host name.",
|
|
839
|
+
status: 400
|
|
840
|
+
});
|
|
733
841
|
let link = null;
|
|
734
842
|
try {
|
|
735
843
|
link = await store.resolve(host, slug);
|
|
736
844
|
} catch {
|
|
737
|
-
return
|
|
845
|
+
return publicShortlinkError("not_found", slug, host);
|
|
738
846
|
}
|
|
739
847
|
if (!link)
|
|
740
|
-
return
|
|
848
|
+
return publicShortlinkError("not_found", slug, host);
|
|
741
849
|
if (!link.active)
|
|
742
|
-
return
|
|
850
|
+
return publicShortlinkError("disabled", slug, host);
|
|
743
851
|
if (isExpired(link))
|
|
744
|
-
return
|
|
852
|
+
return publicShortlinkError("expired", slug, host);
|
|
745
853
|
if (link.max_uses !== null && link.used_count >= link.max_uses) {
|
|
746
|
-
return
|
|
854
|
+
return publicShortlinkError("used", slug, host);
|
|
855
|
+
}
|
|
856
|
+
if (request.method.toUpperCase() === "HEAD") {
|
|
857
|
+
return new Response(null, {
|
|
858
|
+
status: redirectStatus,
|
|
859
|
+
headers: { location: link.destination_url }
|
|
860
|
+
});
|
|
747
861
|
}
|
|
748
862
|
const consumed = store.consumeLinkUse ? await store.consumeLinkUse(link) : link;
|
|
749
863
|
if (!consumed) {
|
|
750
|
-
return
|
|
864
|
+
return publicShortlinkError("used", slug, host);
|
|
751
865
|
}
|
|
752
866
|
await store.recordClick(consumed, {
|
|
753
867
|
ip: getClientIp(request),
|
package/package.json
CHANGED