@hasna/shortlinks 0.1.20 → 0.1.22

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
@@ -3271,7 +3271,8 @@ import { mkdirSync as mkdirSync2 } from "fs";
3271
3271
  import { dirname as dirname2 } from "path";
3272
3272
 
3273
3273
  // src/config.ts
3274
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
3274
+ import { existsSync as existsSync2, linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
3275
+ import { randomBytes } from "crypto";
3275
3276
  import { homedir as homedir2 } from "os";
3276
3277
  import { dirname, join as join2, resolve } from "path";
3277
3278
  var SERVICE_NAME = "shortlinks";
@@ -3287,6 +3288,9 @@ function ensureDataDir() {
3287
3288
  function getConfigPath() {
3288
3289
  return join2(ensureDataDir(), "config.json");
3289
3290
  }
3291
+ function getClickSaltPath() {
3292
+ return join2(ensureDataDir(), "click-salt");
3293
+ }
3290
3294
  function getDatabasePath(explicitPath) {
3291
3295
  if (explicitPath)
3292
3296
  return resolve(explicitPath);
@@ -3294,6 +3298,51 @@ function getDatabasePath(explicitPath) {
3294
3298
  return resolve(process.env.SHORTLINKS_DB);
3295
3299
  return join2(ensureDataDir(), `${SERVICE_NAME}.db`);
3296
3300
  }
3301
+ function readClickSaltFile(path) {
3302
+ try {
3303
+ const saved = readFileSync(path, "utf-8").trim();
3304
+ return saved || null;
3305
+ } catch {
3306
+ return null;
3307
+ }
3308
+ }
3309
+ function clickSaltError(path, error) {
3310
+ const detail = error instanceof Error ? error.message : String(error);
3311
+ return new Error(`Could not initialize click salt at ${path}. Set SHORTLINKS_CLICK_SALT or fix data directory permissions. ${detail}`);
3312
+ }
3313
+ function getClickSalt() {
3314
+ const explicit = process.env.SHORTLINKS_CLICK_SALT?.trim();
3315
+ if (explicit)
3316
+ return explicit;
3317
+ const path = getClickSaltPath();
3318
+ const saved = readClickSaltFile(path);
3319
+ if (saved)
3320
+ return saved;
3321
+ const generated = randomBytes(32).toString("hex");
3322
+ const tempPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
3323
+ try {
3324
+ writeFileSync(tempPath, `${generated}
3325
+ `, { flag: "wx", mode: 384 });
3326
+ try {
3327
+ linkSync(tempPath, path);
3328
+ return generated;
3329
+ } catch (error) {
3330
+ const winner = readClickSaltFile(path);
3331
+ if (winner)
3332
+ return winner;
3333
+ throw clickSaltError(path, error);
3334
+ } finally {
3335
+ try {
3336
+ unlinkSync(tempPath);
3337
+ } catch {}
3338
+ }
3339
+ } catch (error) {
3340
+ const winner = readClickSaltFile(path);
3341
+ if (winner)
3342
+ return winner;
3343
+ throw clickSaltError(path, error);
3344
+ }
3345
+ }
3297
3346
  function loadConfig() {
3298
3347
  const path = getConfigPath();
3299
3348
  if (!existsSync2(path))
@@ -3335,7 +3384,9 @@ function normalizeHostname(input) {
3335
3384
  throw new Error(`Invalid domain: ${input}`);
3336
3385
  }
3337
3386
  hostname = hostname.replace(/\.$/, "");
3338
- if (!/^[a-z0-9.-]+$/.test(hostname) || hostname.includes("..")) {
3387
+ const labels = hostname.split(".");
3388
+ const labelsAreValid = labels.every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9-]+$/.test(label) && !label.startsWith("-") && !label.endsWith("-"));
3389
+ if (hostname.length > 253 || !labelsAreValid) {
3339
3390
  throw new Error(`Invalid domain: ${input}`);
3340
3391
  }
3341
3392
  return hostname;
@@ -3463,13 +3514,13 @@ import { hostname } from "os";
3463
3514
  import { join as join3 } from "path";
3464
3515
 
3465
3516
  // src/slug.ts
3466
- import { randomBytes } from "crypto";
3517
+ import { randomBytes as randomBytes2 } from "crypto";
3467
3518
  var SLUG_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
3468
3519
  var DEFAULT_SLUG_LENGTH = 7;
3469
3520
  function randomToken(length = DEFAULT_SLUG_LENGTH) {
3470
3521
  if (length < 1 || length > 128)
3471
3522
  throw new Error("Token length must be between 1 and 128.");
3472
- const bytes = randomBytes(length);
3523
+ const bytes = randomBytes2(length);
3473
3524
  let out = "";
3474
3525
  for (let i = 0;i < length; i += 1) {
3475
3526
  out += SLUG_ALPHABET[bytes[i] % SLUG_ALPHABET.length];
@@ -3789,8 +3840,7 @@ class ShortlinksStore {
3789
3840
  throw new Error("Could not generate an unused slug after 32 attempts.");
3790
3841
  }
3791
3842
  hashIp(ip) {
3792
- const salt = process.env.SHORTLINKS_CLICK_SALT || "shortlinks-local";
3793
- return createHash("sha256").update(`${salt}:${ip}`).digest("hex");
3843
+ return createHash("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
3794
3844
  }
3795
3845
  }
3796
3846
 
@@ -4098,8 +4148,7 @@ class PgShortlinksStore {
4098
4148
  };
4099
4149
  }
4100
4150
  hashIp(ip) {
4101
- const salt = process.env.SHORTLINKS_CLICK_SALT || "shortlinks-cloud";
4102
- return createHash2("sha256").update(`${salt}:${ip}`).digest("hex");
4151
+ return createHash2("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
4103
4152
  }
4104
4153
  async generateAvailableSlug(domainId, length) {
4105
4154
  for (let attempt = 0;attempt < 32; attempt += 1) {
@@ -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/config.d.ts CHANGED
@@ -12,7 +12,9 @@ export interface ShortlinksConfig {
12
12
  export declare function getDataDir(): string;
13
13
  export declare function ensureDataDir(): string;
14
14
  export declare function getConfigPath(): string;
15
+ export declare function getClickSaltPath(): string;
15
16
  export declare function getDatabasePath(explicitPath?: string): string;
17
+ export declare function getClickSalt(): string;
16
18
  export declare function loadConfig(): ShortlinksConfig;
17
19
  export declare function saveConfig(config: ShortlinksConfig): void;
18
20
  export declare function updateConfig(patch: ShortlinksConfig): ShortlinksConfig;
package/dist/index.js CHANGED
@@ -7,7 +7,8 @@ import { mkdirSync as mkdirSync2 } from "fs";
7
7
  import { dirname as dirname2 } from "path";
8
8
 
9
9
  // src/config.ts
10
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
10
+ import { existsSync, linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
11
+ import { randomBytes } from "crypto";
11
12
  import { homedir } from "os";
12
13
  import { dirname, join, resolve } from "path";
13
14
  var SERVICE_NAME = "shortlinks";
@@ -23,6 +24,9 @@ function ensureDataDir() {
23
24
  function getConfigPath() {
24
25
  return join(ensureDataDir(), "config.json");
25
26
  }
27
+ function getClickSaltPath() {
28
+ return join(ensureDataDir(), "click-salt");
29
+ }
26
30
  function getDatabasePath(explicitPath) {
27
31
  if (explicitPath)
28
32
  return resolve(explicitPath);
@@ -30,6 +34,51 @@ function getDatabasePath(explicitPath) {
30
34
  return resolve(process.env.SHORTLINKS_DB);
31
35
  return join(ensureDataDir(), `${SERVICE_NAME}.db`);
32
36
  }
37
+ function readClickSaltFile(path) {
38
+ try {
39
+ const saved = readFileSync(path, "utf-8").trim();
40
+ return saved || null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ function clickSaltError(path, error) {
46
+ const detail = error instanceof Error ? error.message : String(error);
47
+ return new Error(`Could not initialize click salt at ${path}. Set SHORTLINKS_CLICK_SALT or fix data directory permissions. ${detail}`);
48
+ }
49
+ function getClickSalt() {
50
+ const explicit = process.env.SHORTLINKS_CLICK_SALT?.trim();
51
+ if (explicit)
52
+ return explicit;
53
+ const path = getClickSaltPath();
54
+ const saved = readClickSaltFile(path);
55
+ if (saved)
56
+ return saved;
57
+ const generated = randomBytes(32).toString("hex");
58
+ const tempPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
59
+ try {
60
+ writeFileSync(tempPath, `${generated}
61
+ `, { flag: "wx", mode: 384 });
62
+ try {
63
+ linkSync(tempPath, path);
64
+ return generated;
65
+ } catch (error) {
66
+ const winner = readClickSaltFile(path);
67
+ if (winner)
68
+ return winner;
69
+ throw clickSaltError(path, error);
70
+ } finally {
71
+ try {
72
+ unlinkSync(tempPath);
73
+ } catch {}
74
+ }
75
+ } catch (error) {
76
+ const winner = readClickSaltFile(path);
77
+ if (winner)
78
+ return winner;
79
+ throw clickSaltError(path, error);
80
+ }
81
+ }
33
82
  function loadConfig() {
34
83
  const path = getConfigPath();
35
84
  if (!existsSync(path))
@@ -71,7 +120,9 @@ function normalizeHostname(input) {
71
120
  throw new Error(`Invalid domain: ${input}`);
72
121
  }
73
122
  hostname = hostname.replace(/\.$/, "");
74
- if (!/^[a-z0-9.-]+$/.test(hostname) || hostname.includes("..")) {
123
+ const labels = hostname.split(".");
124
+ const labelsAreValid = labels.every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9-]+$/.test(label) && !label.startsWith("-") && !label.endsWith("-"));
125
+ if (hostname.length > 253 || !labelsAreValid) {
75
126
  throw new Error(`Invalid domain: ${input}`);
76
127
  }
77
128
  return hostname;
@@ -201,13 +252,13 @@ import { hostname } from "os";
201
252
  import { join as join2 } from "path";
202
253
 
203
254
  // src/slug.ts
204
- import { randomBytes } from "crypto";
255
+ import { randomBytes as randomBytes2 } from "crypto";
205
256
  var SLUG_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
206
257
  var DEFAULT_SLUG_LENGTH = 7;
207
258
  function randomToken(length = DEFAULT_SLUG_LENGTH) {
208
259
  if (length < 1 || length > 128)
209
260
  throw new Error("Token length must be between 1 and 128.");
210
- const bytes = randomBytes(length);
261
+ const bytes = randomBytes2(length);
211
262
  let out = "";
212
263
  for (let i = 0;i < length; i += 1) {
213
264
  out += SLUG_ALPHABET[bytes[i] % SLUG_ALPHABET.length];
@@ -527,8 +578,7 @@ class ShortlinksStore {
527
578
  throw new Error("Could not generate an unused slug after 32 attempts.");
528
579
  }
529
580
  hashIp(ip) {
530
- const salt = process.env.SHORTLINKS_CLICK_SALT || "shortlinks-local";
531
- return createHash("sha256").update(`${salt}:${ip}`).digest("hex");
581
+ return createHash("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
532
582
  }
533
583
  }
534
584
  // src/pg-store.ts
@@ -835,8 +885,7 @@ class PgShortlinksStore {
835
885
  };
836
886
  }
837
887
  hashIp(ip) {
838
- const salt = process.env.SHORTLINKS_CLICK_SALT || "shortlinks-cloud";
839
- return createHash2("sha256").update(`${salt}:${ip}`).digest("hex");
888
+ return createHash2("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
840
889
  }
841
890
  async generateAvailableSlug(domainId, length) {
842
891
  for (let attempt = 0;attempt < 32; attempt += 1) {
package/dist/server.js CHANGED
@@ -8,7 +8,8 @@ import { mkdirSync as mkdirSync2 } from "fs";
8
8
  import { dirname as dirname2 } from "path";
9
9
 
10
10
  // src/config.ts
11
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
11
+ import { existsSync, linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
12
+ import { randomBytes } from "crypto";
12
13
  import { homedir } from "os";
13
14
  import { dirname, join, resolve } from "path";
14
15
  var SERVICE_NAME = "shortlinks";
@@ -24,6 +25,9 @@ function ensureDataDir() {
24
25
  function getConfigPath() {
25
26
  return join(ensureDataDir(), "config.json");
26
27
  }
28
+ function getClickSaltPath() {
29
+ return join(ensureDataDir(), "click-salt");
30
+ }
27
31
  function getDatabasePath(explicitPath) {
28
32
  if (explicitPath)
29
33
  return resolve(explicitPath);
@@ -31,6 +35,51 @@ function getDatabasePath(explicitPath) {
31
35
  return resolve(process.env.SHORTLINKS_DB);
32
36
  return join(ensureDataDir(), `${SERVICE_NAME}.db`);
33
37
  }
38
+ function readClickSaltFile(path) {
39
+ try {
40
+ const saved = readFileSync(path, "utf-8").trim();
41
+ return saved || null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ function clickSaltError(path, error) {
47
+ const detail = error instanceof Error ? error.message : String(error);
48
+ return new Error(`Could not initialize click salt at ${path}. Set SHORTLINKS_CLICK_SALT or fix data directory permissions. ${detail}`);
49
+ }
50
+ function getClickSalt() {
51
+ const explicit = process.env.SHORTLINKS_CLICK_SALT?.trim();
52
+ if (explicit)
53
+ return explicit;
54
+ const path = getClickSaltPath();
55
+ const saved = readClickSaltFile(path);
56
+ if (saved)
57
+ return saved;
58
+ const generated = randomBytes(32).toString("hex");
59
+ const tempPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
60
+ try {
61
+ writeFileSync(tempPath, `${generated}
62
+ `, { flag: "wx", mode: 384 });
63
+ try {
64
+ linkSync(tempPath, path);
65
+ return generated;
66
+ } catch (error) {
67
+ const winner = readClickSaltFile(path);
68
+ if (winner)
69
+ return winner;
70
+ throw clickSaltError(path, error);
71
+ } finally {
72
+ try {
73
+ unlinkSync(tempPath);
74
+ } catch {}
75
+ }
76
+ } catch (error) {
77
+ const winner = readClickSaltFile(path);
78
+ if (winner)
79
+ return winner;
80
+ throw clickSaltError(path, error);
81
+ }
82
+ }
34
83
  function loadConfig() {
35
84
  const path = getConfigPath();
36
85
  if (!existsSync(path))
@@ -72,7 +121,9 @@ function normalizeHostname(input) {
72
121
  throw new Error(`Invalid domain: ${input}`);
73
122
  }
74
123
  hostname = hostname.replace(/\.$/, "");
75
- if (!/^[a-z0-9.-]+$/.test(hostname) || hostname.includes("..")) {
124
+ const labels = hostname.split(".");
125
+ const labelsAreValid = labels.every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9-]+$/.test(label) && !label.startsWith("-") && !label.endsWith("-"));
126
+ if (hostname.length > 253 || !labelsAreValid) {
76
127
  throw new Error(`Invalid domain: ${input}`);
77
128
  }
78
129
  return hostname;
@@ -200,13 +251,13 @@ import { hostname } from "os";
200
251
  import { join as join2 } from "path";
201
252
 
202
253
  // src/slug.ts
203
- import { randomBytes } from "crypto";
254
+ import { randomBytes as randomBytes2 } from "crypto";
204
255
  var SLUG_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
205
256
  var DEFAULT_SLUG_LENGTH = 7;
206
257
  function randomToken(length = DEFAULT_SLUG_LENGTH) {
207
258
  if (length < 1 || length > 128)
208
259
  throw new Error("Token length must be between 1 and 128.");
209
- const bytes = randomBytes(length);
260
+ const bytes = randomBytes2(length);
210
261
  let out = "";
211
262
  for (let i = 0;i < length; i += 1) {
212
263
  out += SLUG_ALPHABET[bytes[i] % SLUG_ALPHABET.length];
@@ -526,8 +577,7 @@ class ShortlinksStore {
526
577
  throw new Error("Could not generate an unused slug after 32 attempts.");
527
578
  }
528
579
  hashIp(ip) {
529
- const salt = process.env.SHORTLINKS_CLICK_SALT || "shortlinks-local";
530
- return createHash("sha256").update(`${salt}:${ip}`).digest("hex");
580
+ return createHash("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
531
581
  }
532
582
  }
533
583
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/shortlinks",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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",