@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 +57 -8
- package/dist/cloudflare.js +3 -1
- package/dist/config.d.ts +2 -0
- package/dist/index.js +57 -8
- package/dist/server.js +56 -6
- 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))
|
|
@@ -3335,7 +3384,9 @@ function normalizeHostname(input) {
|
|
|
3335
3384
|
throw new Error(`Invalid domain: ${input}`);
|
|
3336
3385
|
}
|
|
3337
3386
|
hostname = hostname.replace(/\.$/, "");
|
|
3338
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/cloudflare.js
CHANGED
|
@@ -20,7 +20,9 @@ function normalizeHostname(input) {
|
|
|
20
20
|
throw new Error(`Invalid domain: ${input}`);
|
|
21
21
|
}
|
|
22
22
|
hostname = hostname.replace(/\.$/, "");
|
|
23
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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