@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 +54 -7
- package/dist/config.d.ts +2 -0
- package/dist/index.js +54 -7
- package/dist/server.js +53 -5
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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