@hasna/shortlinks 0.1.21 → 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))
@@ -3465,13 +3514,13 @@ import { hostname } from "os";
3465
3514
  import { join as join3 } from "path";
3466
3515
 
3467
3516
  // src/slug.ts
3468
- import { randomBytes } from "crypto";
3517
+ import { randomBytes as randomBytes2 } from "crypto";
3469
3518
  var SLUG_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
3470
3519
  var DEFAULT_SLUG_LENGTH = 7;
3471
3520
  function randomToken(length = DEFAULT_SLUG_LENGTH) {
3472
3521
  if (length < 1 || length > 128)
3473
3522
  throw new Error("Token length must be between 1 and 128.");
3474
- const bytes = randomBytes(length);
3523
+ const bytes = randomBytes2(length);
3475
3524
  let out = "";
3476
3525
  for (let i = 0;i < length; i += 1) {
3477
3526
  out += SLUG_ALPHABET[bytes[i] % SLUG_ALPHABET.length];
@@ -3791,8 +3840,7 @@ class ShortlinksStore {
3791
3840
  throw new Error("Could not generate an unused slug after 32 attempts.");
3792
3841
  }
3793
3842
  hashIp(ip) {
3794
- const salt = process.env.SHORTLINKS_CLICK_SALT || "shortlinks-local";
3795
- return createHash("sha256").update(`${salt}:${ip}`).digest("hex");
3843
+ return createHash("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
3796
3844
  }
3797
3845
  }
3798
3846
 
@@ -4100,8 +4148,7 @@ class PgShortlinksStore {
4100
4148
  };
4101
4149
  }
4102
4150
  hashIp(ip) {
4103
- const salt = process.env.SHORTLINKS_CLICK_SALT || "shortlinks-cloud";
4104
- return createHash2("sha256").update(`${salt}:${ip}`).digest("hex");
4151
+ return createHash2("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
4105
4152
  }
4106
4153
  async generateAvailableSlug(domainId, length) {
4107
4154
  for (let attempt = 0;attempt < 32; attempt += 1) {
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))
@@ -203,13 +252,13 @@ import { hostname } from "os";
203
252
  import { join as join2 } from "path";
204
253
 
205
254
  // src/slug.ts
206
- import { randomBytes } from "crypto";
255
+ import { randomBytes as randomBytes2 } from "crypto";
207
256
  var SLUG_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
208
257
  var DEFAULT_SLUG_LENGTH = 7;
209
258
  function randomToken(length = DEFAULT_SLUG_LENGTH) {
210
259
  if (length < 1 || length > 128)
211
260
  throw new Error("Token length must be between 1 and 128.");
212
- const bytes = randomBytes(length);
261
+ const bytes = randomBytes2(length);
213
262
  let out = "";
214
263
  for (let i = 0;i < length; i += 1) {
215
264
  out += SLUG_ALPHABET[bytes[i] % SLUG_ALPHABET.length];
@@ -529,8 +578,7 @@ class ShortlinksStore {
529
578
  throw new Error("Could not generate an unused slug after 32 attempts.");
530
579
  }
531
580
  hashIp(ip) {
532
- const salt = process.env.SHORTLINKS_CLICK_SALT || "shortlinks-local";
533
- return createHash("sha256").update(`${salt}:${ip}`).digest("hex");
581
+ return createHash("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
534
582
  }
535
583
  }
536
584
  // src/pg-store.ts
@@ -837,8 +885,7 @@ class PgShortlinksStore {
837
885
  };
838
886
  }
839
887
  hashIp(ip) {
840
- const salt = process.env.SHORTLINKS_CLICK_SALT || "shortlinks-cloud";
841
- return createHash2("sha256").update(`${salt}:${ip}`).digest("hex");
888
+ return createHash2("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
842
889
  }
843
890
  async generateAvailableSlug(domainId, length) {
844
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))
@@ -202,13 +251,13 @@ import { hostname } from "os";
202
251
  import { join as join2 } from "path";
203
252
 
204
253
  // src/slug.ts
205
- import { randomBytes } from "crypto";
254
+ import { randomBytes as randomBytes2 } from "crypto";
206
255
  var SLUG_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
207
256
  var DEFAULT_SLUG_LENGTH = 7;
208
257
  function randomToken(length = DEFAULT_SLUG_LENGTH) {
209
258
  if (length < 1 || length > 128)
210
259
  throw new Error("Token length must be between 1 and 128.");
211
- const bytes = randomBytes(length);
260
+ const bytes = randomBytes2(length);
212
261
  let out = "";
213
262
  for (let i = 0;i < length; i += 1) {
214
263
  out += SLUG_ALPHABET[bytes[i] % SLUG_ALPHABET.length];
@@ -528,8 +577,7 @@ class ShortlinksStore {
528
577
  throw new Error("Could not generate an unused slug after 32 attempts.");
529
578
  }
530
579
  hashIp(ip) {
531
- const salt = process.env.SHORTLINKS_CLICK_SALT || "shortlinks-local";
532
- return createHash("sha256").update(`${salt}:${ip}`).digest("hex");
580
+ return createHash("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
533
581
  }
534
582
  }
535
583
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/shortlinks",
3
- "version": "0.1.21",
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",