@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 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 json({ error: "Invalid slug." }, 400);
9959
+ return publicShortlinkError("invalid", "");
9856
9960
  }
9857
9961
  if (!slug)
9858
- return json({ error: "Missing slug." }, 404);
9962
+ return publicShortlinkError("missing", "");
9859
9963
  if (reservedPathPrefixes.has(slug.toLowerCase())) {
9860
- return json({ error: "Reserved path prefix.", slug }, 404);
9964
+ return publicShortlinkError("reserved", slug);
9861
9965
  }
9862
9966
  const host = getHost(request, options.defaultHost);
9863
9967
  if (!host)
9864
- return json({ error: "Missing Host header." }, 400);
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 json({ error: "Shortlink not found.", slug, host }, 404);
9977
+ return publicShortlinkError("not_found", slug, host);
9870
9978
  }
9871
9979
  if (!link)
9872
- return json({ error: "Shortlink not found.", slug, host }, 404);
9980
+ return publicShortlinkError("not_found", slug, host);
9873
9981
  if (!link.active)
9874
- return json({ error: "Shortlink is disabled.", slug, host }, 410);
9982
+ return publicShortlinkError("disabled", slug, host);
9875
9983
  if (isExpired(link))
9876
- return json({ error: "Shortlink is expired.", slug, host }, 410);
9984
+ return publicShortlinkError("expired", slug, host);
9877
9985
  if (link.max_uses !== null && link.used_count >= link.max_uses) {
9878
- return json({ error: "Shortlink max uses reached.", slug, host }, 410);
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 json({ error: "Shortlink max uses reached.", slug, host }, 410);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 json({ error: "Invalid slug." }, 400);
6540
+ return publicShortlinkError("invalid", "");
6437
6541
  }
6438
6542
  if (!slug)
6439
- return json({ error: "Missing slug." }, 404);
6543
+ return publicShortlinkError("missing", "");
6440
6544
  if (reservedPathPrefixes.has(slug.toLowerCase())) {
6441
- return json({ error: "Reserved path prefix.", slug }, 404);
6545
+ return publicShortlinkError("reserved", slug);
6442
6546
  }
6443
6547
  const host = getHost(request, options.defaultHost);
6444
6548
  if (!host)
6445
- return json({ error: "Missing Host header." }, 400);
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 json({ error: "Shortlink not found.", slug, host }, 404);
6558
+ return publicShortlinkError("not_found", slug, host);
6451
6559
  }
6452
6560
  if (!link)
6453
- return json({ error: "Shortlink not found.", slug, host }, 404);
6561
+ return publicShortlinkError("not_found", slug, host);
6454
6562
  if (!link.active)
6455
- return json({ error: "Shortlink is disabled.", slug, host }, 410);
6563
+ return publicShortlinkError("disabled", slug, host);
6456
6564
  if (isExpired(link))
6457
- return json({ error: "Shortlink is expired.", slug, host }, 410);
6565
+ return publicShortlinkError("expired", slug, host);
6458
6566
  if (link.max_uses !== null && link.used_count >= link.max_uses) {
6459
- return json({ error: "Shortlink max uses reached.", slug, host }, 410);
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 json({ error: "Shortlink max uses reached.", slug, host }, 410);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 json({ error: "Invalid slug." }, 400);
827
+ return publicShortlinkError("invalid", "");
724
828
  }
725
829
  if (!slug)
726
- return json({ error: "Missing slug." }, 404);
830
+ return publicShortlinkError("missing", "");
727
831
  if (reservedPathPrefixes.has(slug.toLowerCase())) {
728
- return json({ error: "Reserved path prefix.", slug }, 404);
832
+ return publicShortlinkError("reserved", slug);
729
833
  }
730
834
  const host = getHost(request, options.defaultHost);
731
835
  if (!host)
732
- return json({ error: "Missing Host header." }, 400);
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 json({ error: "Shortlink not found.", slug, host }, 404);
845
+ return publicShortlinkError("not_found", slug, host);
738
846
  }
739
847
  if (!link)
740
- return json({ error: "Shortlink not found.", slug, host }, 404);
848
+ return publicShortlinkError("not_found", slug, host);
741
849
  if (!link.active)
742
- return json({ error: "Shortlink is disabled.", slug, host }, 410);
850
+ return publicShortlinkError("disabled", slug, host);
743
851
  if (isExpired(link))
744
- return json({ error: "Shortlink is expired.", slug, host }, 410);
852
+ return publicShortlinkError("expired", slug, host);
745
853
  if (link.max_uses !== null && link.used_count >= link.max_uses) {
746
- return json({ error: "Shortlink max uses reached.", slug, host }, 410);
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 json({ error: "Shortlink max uses reached.", slug, host }, 410);
864
+ return publicShortlinkError("used", slug, host);
751
865
  }
752
866
  await store.recordClick(consumed, {
753
867
  ip: getClientIp(request),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/shortlinks",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "CLI-only shortlink manager for custom domains, click tracking, Cloudflare setup, and repo-native storage sync",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",