@hasna/shortlinks 0.1.19 → 0.1.21

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
@@ -3335,7 +3335,9 @@ function normalizeHostname(input) {
3335
3335
  throw new Error(`Invalid domain: ${input}`);
3336
3336
  }
3337
3337
  hostname = hostname.replace(/\.$/, "");
3338
- if (!/^[a-z0-9.-]+$/.test(hostname) || hostname.includes("..")) {
3338
+ const labels = hostname.split(".");
3339
+ const labelsAreValid = labels.every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9-]+$/.test(label) && !label.startsWith("-") && !label.endsWith("-"));
3340
+ if (hostname.length > 253 || !labelsAreValid) {
3339
3341
  throw new Error(`Invalid domain: ${input}`);
3340
3342
  }
3341
3343
  return hostname;
@@ -4138,6 +4140,9 @@ function getClientIp(request) {
4138
4140
  function isExpired(link) {
4139
4141
  return Boolean(link.expires_at && new Date(link.expires_at).getTime() <= Date.now());
4140
4142
  }
4143
+ function logRecordClickError(link) {
4144
+ console.error(`[shortlinks] Click analytics recording failed for ${link.hostname}/${link.slug}.`);
4145
+ }
4141
4146
  function createShortlinksHandler(options = {}) {
4142
4147
  const store = options.store || new ShortlinksStore(options.dbPath);
4143
4148
  const redirectStatus = options.redirectStatus || 302;
@@ -4176,16 +4181,28 @@ function createShortlinksHandler(options = {}) {
4176
4181
  if (isExpired(link))
4177
4182
  return json({ error: "Shortlink is expired.", slug, host }, 410);
4178
4183
  if (request.method === "GET") {
4179
- await store.recordClick(link, {
4180
- ip: getClientIp(request),
4181
- userAgent: request.headers.get("user-agent"),
4182
- referer: request.headers.get("referer"),
4183
- country: request.headers.get("cf-ipcountry"),
4184
- metadata: {
4185
- path: url.pathname,
4186
- query: url.search
4184
+ try {
4185
+ await store.recordClick(link, {
4186
+ ip: getClientIp(request),
4187
+ userAgent: request.headers.get("user-agent"),
4188
+ referer: request.headers.get("referer"),
4189
+ country: request.headers.get("cf-ipcountry"),
4190
+ metadata: {
4191
+ path: url.pathname,
4192
+ query: url.search
4193
+ }
4194
+ });
4195
+ } catch (error) {
4196
+ if (options.onRecordClickError) {
4197
+ try {
4198
+ await options.onRecordClickError(error, { link, request });
4199
+ } catch {
4200
+ logRecordClickError(link);
4201
+ }
4202
+ } else {
4203
+ logRecordClickError(link);
4187
4204
  }
4188
- });
4205
+ }
4189
4206
  }
4190
4207
  return Response.redirect(link.destination_url, redirectStatus);
4191
4208
  };
@@ -20,7 +20,9 @@ function normalizeHostname(input) {
20
20
  throw new Error(`Invalid domain: ${input}`);
21
21
  }
22
22
  hostname = hostname.replace(/\.$/, "");
23
- if (!/^[a-z0-9.-]+$/.test(hostname) || hostname.includes("..")) {
23
+ const labels = hostname.split(".");
24
+ const labelsAreValid = labels.every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9-]+$/.test(label) && !label.startsWith("-") && !label.endsWith("-"));
25
+ if (hostname.length > 253 || !labelsAreValid) {
24
26
  throw new Error(`Invalid domain: ${input}`);
25
27
  }
26
28
  return hostname;
package/dist/index.js CHANGED
@@ -71,7 +71,9 @@ function normalizeHostname(input) {
71
71
  throw new Error(`Invalid domain: ${input}`);
72
72
  }
73
73
  hostname = hostname.replace(/\.$/, "");
74
- if (!/^[a-z0-9.-]+$/.test(hostname) || hostname.includes("..")) {
74
+ const labels = hostname.split(".");
75
+ const labelsAreValid = labels.every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9-]+$/.test(label) && !label.startsWith("-") && !label.endsWith("-"));
76
+ if (hostname.length > 253 || !labelsAreValid) {
75
77
  throw new Error(`Invalid domain: ${input}`);
76
78
  }
77
79
  return hostname;
@@ -874,6 +876,9 @@ function getClientIp(request) {
874
876
  function isExpired(link) {
875
877
  return Boolean(link.expires_at && new Date(link.expires_at).getTime() <= Date.now());
876
878
  }
879
+ function logRecordClickError(link) {
880
+ console.error(`[shortlinks] Click analytics recording failed for ${link.hostname}/${link.slug}.`);
881
+ }
877
882
  function createShortlinksHandler(options = {}) {
878
883
  const store = options.store || new ShortlinksStore(options.dbPath);
879
884
  const redirectStatus = options.redirectStatus || 302;
@@ -912,16 +917,28 @@ function createShortlinksHandler(options = {}) {
912
917
  if (isExpired(link))
913
918
  return json({ error: "Shortlink is expired.", slug, host }, 410);
914
919
  if (request.method === "GET") {
915
- await store.recordClick(link, {
916
- ip: getClientIp(request),
917
- userAgent: request.headers.get("user-agent"),
918
- referer: request.headers.get("referer"),
919
- country: request.headers.get("cf-ipcountry"),
920
- metadata: {
921
- path: url.pathname,
922
- query: url.search
920
+ try {
921
+ await store.recordClick(link, {
922
+ ip: getClientIp(request),
923
+ userAgent: request.headers.get("user-agent"),
924
+ referer: request.headers.get("referer"),
925
+ country: request.headers.get("cf-ipcountry"),
926
+ metadata: {
927
+ path: url.pathname,
928
+ query: url.search
929
+ }
930
+ });
931
+ } catch (error) {
932
+ if (options.onRecordClickError) {
933
+ try {
934
+ await options.onRecordClickError(error, { link, request });
935
+ } catch {
936
+ logRecordClickError(link);
937
+ }
938
+ } else {
939
+ logRecordClickError(link);
923
940
  }
924
- });
941
+ }
925
942
  }
926
943
  return Response.redirect(link.destination_url, redirectStatus);
927
944
  };
package/dist/server.d.ts CHANGED
@@ -12,11 +12,16 @@ export interface ShortlinksRuntimeStore {
12
12
  resolve(hostname: string, slug: string): Link | null | Promise<Link | null>;
13
13
  recordClick(link: Link, input?: ClickInput): unknown | Promise<unknown>;
14
14
  }
15
+ export interface RecordClickErrorContext {
16
+ link: Link;
17
+ request: Request;
18
+ }
15
19
  export interface ShortlinksHandlerOptions {
16
20
  store?: ShortlinksRuntimeStore;
17
21
  dbPath?: string;
18
22
  defaultHost?: string;
19
23
  redirectStatus?: 301 | 302 | 307 | 308;
24
+ onRecordClickError?: (error: unknown, context: RecordClickErrorContext) => void | Promise<void>;
20
25
  }
21
26
  export declare function createShortlinksHandler(options?: ShortlinksHandlerOptions): (request: Request) => Response | Promise<Response>;
22
27
  export declare function serveShortlinks(options?: ShortlinksHandlerOptions & {
package/dist/server.js CHANGED
@@ -72,7 +72,9 @@ function normalizeHostname(input) {
72
72
  throw new Error(`Invalid domain: ${input}`);
73
73
  }
74
74
  hostname = hostname.replace(/\.$/, "");
75
- if (!/^[a-z0-9.-]+$/.test(hostname) || hostname.includes("..")) {
75
+ const labels = hostname.split(".");
76
+ const labelsAreValid = labels.every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9-]+$/.test(label) && !label.startsWith("-") && !label.endsWith("-"));
77
+ if (hostname.length > 253 || !labelsAreValid) {
76
78
  throw new Error(`Invalid domain: ${input}`);
77
79
  }
78
80
  return hostname;
@@ -555,6 +557,9 @@ function getClientIp(request) {
555
557
  function isExpired(link) {
556
558
  return Boolean(link.expires_at && new Date(link.expires_at).getTime() <= Date.now());
557
559
  }
560
+ function logRecordClickError(link) {
561
+ console.error(`[shortlinks] Click analytics recording failed for ${link.hostname}/${link.slug}.`);
562
+ }
558
563
  function createShortlinksHandler(options = {}) {
559
564
  const store = options.store || new ShortlinksStore(options.dbPath);
560
565
  const redirectStatus = options.redirectStatus || 302;
@@ -593,16 +598,28 @@ function createShortlinksHandler(options = {}) {
593
598
  if (isExpired(link))
594
599
  return json({ error: "Shortlink is expired.", slug, host }, 410);
595
600
  if (request.method === "GET") {
596
- await store.recordClick(link, {
597
- ip: getClientIp(request),
598
- userAgent: request.headers.get("user-agent"),
599
- referer: request.headers.get("referer"),
600
- country: request.headers.get("cf-ipcountry"),
601
- metadata: {
602
- path: url.pathname,
603
- query: url.search
601
+ try {
602
+ await store.recordClick(link, {
603
+ ip: getClientIp(request),
604
+ userAgent: request.headers.get("user-agent"),
605
+ referer: request.headers.get("referer"),
606
+ country: request.headers.get("cf-ipcountry"),
607
+ metadata: {
608
+ path: url.pathname,
609
+ query: url.search
610
+ }
611
+ });
612
+ } catch (error) {
613
+ if (options.onRecordClickError) {
614
+ try {
615
+ await options.onRecordClickError(error, { link, request });
616
+ } catch {
617
+ logRecordClickError(link);
618
+ }
619
+ } else {
620
+ logRecordClickError(link);
604
621
  }
605
- });
622
+ }
606
623
  }
607
624
  return Response.redirect(link.destination_url, redirectStatus);
608
625
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/shortlinks",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "CLI-only shortlink manager for custom domains, click tracking, Cloudflare setup, and @hasna cloud sync",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",