@hasna/shortlinks 0.1.17 → 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 +118 -10
- package/dist/index.js +118 -10
- package/dist/server.js +118 -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,30 +9956,34 @@ 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);
|
|
9879
9987
|
}
|
|
9880
9988
|
if (request.method.toUpperCase() === "HEAD") {
|
|
9881
9989
|
return new Response(null, {
|
|
@@ -9885,7 +9993,7 @@ function createShortlinksHandler(options = {}) {
|
|
|
9885
9993
|
}
|
|
9886
9994
|
const consumed = store.consumeLinkUse ? await store.consumeLinkUse(link) : link;
|
|
9887
9995
|
if (!consumed) {
|
|
9888
|
-
return
|
|
9996
|
+
return publicShortlinkError("used", slug, host);
|
|
9889
9997
|
}
|
|
9890
9998
|
await store.recordClick(consumed, {
|
|
9891
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,30 +6537,34 @@ 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);
|
|
6460
6568
|
}
|
|
6461
6569
|
if (request.method.toUpperCase() === "HEAD") {
|
|
6462
6570
|
return new Response(null, {
|
|
@@ -6466,7 +6574,7 @@ function createShortlinksHandler(options = {}) {
|
|
|
6466
6574
|
}
|
|
6467
6575
|
const consumed = store.consumeLinkUse ? await store.consumeLinkUse(link) : link;
|
|
6468
6576
|
if (!consumed) {
|
|
6469
|
-
return
|
|
6577
|
+
return publicShortlinkError("used", slug, host);
|
|
6470
6578
|
}
|
|
6471
6579
|
await store.recordClick(consumed, {
|
|
6472
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,30 +824,34 @@ 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);
|
|
747
855
|
}
|
|
748
856
|
if (request.method.toUpperCase() === "HEAD") {
|
|
749
857
|
return new Response(null, {
|
|
@@ -753,7 +861,7 @@ function createShortlinksHandler(options = {}) {
|
|
|
753
861
|
}
|
|
754
862
|
const consumed = store.consumeLinkUse ? await store.consumeLinkUse(link) : link;
|
|
755
863
|
if (!consumed) {
|
|
756
|
-
return
|
|
864
|
+
return publicShortlinkError("used", slug, host);
|
|
757
865
|
}
|
|
758
866
|
await store.recordClick(consumed, {
|
|
759
867
|
ip: getClientIp(request),
|
package/package.json
CHANGED