@hasna/shortlinks 0.1.21 → 0.1.23
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/README.md +14 -19
- package/dist/cli/index.js +392 -78
- package/dist/config.d.ts +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +332 -16
- package/dist/pg-store.d.ts +10 -2
- package/dist/pg-store.js +598 -0
- package/dist/runtime.d.ts +65 -0
- package/dist/runtime.js +181 -0
- package/dist/server.js +53 -5
- package/infra/aws-ec2-user-data.sh +32 -31
- package/package.json +15 -6
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/runtime.ts
|
|
3
|
+
var SHORTLINKS_RUNTIME_ENV = {
|
|
4
|
+
store: "HASNA_SHORTLINKS_STORE",
|
|
5
|
+
databaseUrl: "HASNA_SHORTLINKS_DATABASE_URL",
|
|
6
|
+
databaseSsl: "HASNA_SHORTLINKS_DATABASE_SSL"
|
|
7
|
+
};
|
|
8
|
+
var SHORTLINKS_RUNTIME_FALLBACK_ENV = {
|
|
9
|
+
store: "SHORTLINKS_STORE",
|
|
10
|
+
databaseUrl: "SHORTLINKS_DATABASE_URL",
|
|
11
|
+
databaseSsl: "SHORTLINKS_DATABASE_SSL"
|
|
12
|
+
};
|
|
13
|
+
var CANONICAL_SHORTLINKS_POSTGRES_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
|
|
14
|
+
var CANONICAL_SHORTLINKS_POSTGRES_DATABASE = "shortlinks";
|
|
15
|
+
var CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH = "hasna/xyz/opensource/shortlinks/prod/postgres";
|
|
16
|
+
function getCanonicalShortlinksPostgresConfig() {
|
|
17
|
+
return {
|
|
18
|
+
cluster: CANONICAL_SHORTLINKS_POSTGRES_CLUSTER,
|
|
19
|
+
database: CANONICAL_SHORTLINKS_POSTGRES_DATABASE,
|
|
20
|
+
runtimeSecretPath: CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH,
|
|
21
|
+
primaryEnv: SHORTLINKS_RUNTIME_ENV.databaseUrl,
|
|
22
|
+
fallbackEnv: SHORTLINKS_RUNTIME_FALLBACK_ENV.databaseUrl
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function parseShortlinksStoreMode(value) {
|
|
26
|
+
const normalized = clean(value)?.toLowerCase();
|
|
27
|
+
if (!normalized)
|
|
28
|
+
return "local";
|
|
29
|
+
if (normalized === "local" || normalized === "postgres")
|
|
30
|
+
return normalized;
|
|
31
|
+
if (normalized === "pg")
|
|
32
|
+
return "postgres";
|
|
33
|
+
throw new Error(`${SHORTLINKS_RUNTIME_ENV.store} must be local or postgres`);
|
|
34
|
+
}
|
|
35
|
+
function getShortlinksStoreMode(env = process.env) {
|
|
36
|
+
return parseShortlinksStoreMode(readRuntimeEnv(env, "store").value);
|
|
37
|
+
}
|
|
38
|
+
function getShortlinksDatabaseUrl(env = process.env) {
|
|
39
|
+
return readRuntimeEnv(env, "databaseUrl").value;
|
|
40
|
+
}
|
|
41
|
+
function getShortlinksDatabaseSsl(env = process.env) {
|
|
42
|
+
return parseBoolean(readRuntimeEnv(env, "databaseSsl").value, true);
|
|
43
|
+
}
|
|
44
|
+
function getShortlinksRuntimeEnvName(env, key) {
|
|
45
|
+
const primary = SHORTLINKS_RUNTIME_ENV[key];
|
|
46
|
+
if (clean(env[primary]))
|
|
47
|
+
return primary;
|
|
48
|
+
return SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
|
|
49
|
+
}
|
|
50
|
+
function loadShortlinksRuntimeConfig(env = process.env) {
|
|
51
|
+
const mode = getShortlinksStoreMode(env);
|
|
52
|
+
const databaseUrl = getShortlinksDatabaseUrl(env);
|
|
53
|
+
return {
|
|
54
|
+
service: "shortlinks",
|
|
55
|
+
mode,
|
|
56
|
+
...databaseUrl ? {
|
|
57
|
+
database: {
|
|
58
|
+
provider: "postgres",
|
|
59
|
+
url: databaseUrl,
|
|
60
|
+
ssl: getShortlinksDatabaseSsl(env)
|
|
61
|
+
}
|
|
62
|
+
} : {}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function assertShortlinksPostgresConfig(config) {
|
|
66
|
+
if (config.mode !== "postgres")
|
|
67
|
+
return;
|
|
68
|
+
if (!config.database?.url) {
|
|
69
|
+
throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseUrl} is required when ${SHORTLINKS_RUNTIME_ENV.store}=postgres`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function getShortlinksRuntimeStatus(env = process.env) {
|
|
73
|
+
const issues = [];
|
|
74
|
+
const warnings = [];
|
|
75
|
+
let config;
|
|
76
|
+
try {
|
|
77
|
+
config = loadShortlinksRuntimeConfig(env);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
80
|
+
config = { service: "shortlinks", mode: "local" };
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
assertShortlinksPostgresConfig(config);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
86
|
+
}
|
|
87
|
+
if (config.mode === "local" && config.database?.url) {
|
|
88
|
+
warnings.push(`${SHORTLINKS_RUNTIME_ENV.store}=local ignores configured Postgres database settings`);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
ok: issues.length === 0,
|
|
92
|
+
service: "shortlinks",
|
|
93
|
+
mode: config.mode,
|
|
94
|
+
local_default: config.mode === "local",
|
|
95
|
+
postgres_enabled: config.mode === "postgres",
|
|
96
|
+
database: {
|
|
97
|
+
configured: Boolean(config.database?.url),
|
|
98
|
+
provider: config.database?.provider ?? null,
|
|
99
|
+
redacted_url: redactDatabaseUrl(config.database?.url),
|
|
100
|
+
ssl: config.database?.ssl ?? null
|
|
101
|
+
},
|
|
102
|
+
env: runtimeEnvStatus(env),
|
|
103
|
+
canonical: getCanonicalShortlinksPostgresConfig(),
|
|
104
|
+
issues,
|
|
105
|
+
warnings,
|
|
106
|
+
no_network: true
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function redactDatabaseUrl(value) {
|
|
110
|
+
if (!value)
|
|
111
|
+
return null;
|
|
112
|
+
try {
|
|
113
|
+
const url = new URL(value);
|
|
114
|
+
if (url.username)
|
|
115
|
+
url.username = "***";
|
|
116
|
+
if (url.password)
|
|
117
|
+
url.password = "***";
|
|
118
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
119
|
+
if (isSensitiveQueryKey(key))
|
|
120
|
+
url.searchParams.set(key, "***");
|
|
121
|
+
}
|
|
122
|
+
return url.toString();
|
|
123
|
+
} catch {
|
|
124
|
+
return "(redacted)";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function runtimeEnvStatus(env) {
|
|
128
|
+
return Object.fromEntries(Object.entries(SHORTLINKS_RUNTIME_ENV).map(([key, name]) => {
|
|
129
|
+
const activeName = getShortlinksRuntimeEnvName(env, key);
|
|
130
|
+
return [
|
|
131
|
+
key,
|
|
132
|
+
{
|
|
133
|
+
name,
|
|
134
|
+
active_name: activeName,
|
|
135
|
+
configured: Boolean(clean(env[activeName]))
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
function readRuntimeEnv(env, key) {
|
|
141
|
+
const primary = SHORTLINKS_RUNTIME_ENV[key];
|
|
142
|
+
const primaryValue = clean(env[primary]);
|
|
143
|
+
if (primaryValue)
|
|
144
|
+
return { name: primary, value: primaryValue };
|
|
145
|
+
const fallback = SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
|
|
146
|
+
return { name: fallback, value: clean(env[fallback]) };
|
|
147
|
+
}
|
|
148
|
+
function parseBoolean(value, fallback) {
|
|
149
|
+
const normalized = clean(value)?.toLowerCase();
|
|
150
|
+
if (!normalized)
|
|
151
|
+
return fallback;
|
|
152
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
153
|
+
return true;
|
|
154
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
155
|
+
return false;
|
|
156
|
+
throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseSsl} must be true or false`);
|
|
157
|
+
}
|
|
158
|
+
function isSensitiveQueryKey(key) {
|
|
159
|
+
return /(?:^|[_-])(pass(?:word)?|pwd|secret|token|credential|auth|api[_-]?key|access[_-]?key)(?:$|[_-])/i.test(key);
|
|
160
|
+
}
|
|
161
|
+
function clean(value) {
|
|
162
|
+
const trimmed = value?.trim();
|
|
163
|
+
return trimmed ? trimmed : undefined;
|
|
164
|
+
}
|
|
165
|
+
export {
|
|
166
|
+
redactDatabaseUrl,
|
|
167
|
+
parseShortlinksStoreMode,
|
|
168
|
+
loadShortlinksRuntimeConfig,
|
|
169
|
+
getShortlinksStoreMode,
|
|
170
|
+
getShortlinksRuntimeStatus,
|
|
171
|
+
getShortlinksRuntimeEnvName,
|
|
172
|
+
getShortlinksDatabaseUrl,
|
|
173
|
+
getShortlinksDatabaseSsl,
|
|
174
|
+
getCanonicalShortlinksPostgresConfig,
|
|
175
|
+
assertShortlinksPostgresConfig,
|
|
176
|
+
SHORTLINKS_RUNTIME_FALLBACK_ENV,
|
|
177
|
+
SHORTLINKS_RUNTIME_ENV,
|
|
178
|
+
CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH,
|
|
179
|
+
CANONICAL_SHORTLINKS_POSTGRES_DATABASE,
|
|
180
|
+
CANONICAL_SHORTLINKS_POSTGRES_CLUSTER
|
|
181
|
+
};
|
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
|
|
|
@@ -4,9 +4,7 @@ set -euo pipefail
|
|
|
4
4
|
export AWS_REGION="${AWS_REGION:-us-east-1}"
|
|
5
5
|
export SHORTLINKS_HOME="/var/lib/shortlinks"
|
|
6
6
|
export SHORTLINKS_PACKAGE="@hasna/shortlinks@latest"
|
|
7
|
-
export
|
|
8
|
-
export RDS_HOST="hasnaxyz-prod-opensource.c4limg0qgqvk.us-east-1.rds.amazonaws.com"
|
|
9
|
-
export RDS_USERNAME="hasna_admin"
|
|
7
|
+
export SHORTLINKS_DATABASE_SECRET_ID="${SHORTLINKS_DATABASE_SECRET_ID:-hasna/xyz/opensource/shortlinks/prod/postgres}"
|
|
10
8
|
|
|
11
9
|
dnf update -y
|
|
12
10
|
dnf install -y awscli jq tar gzip shadow-utils libcap
|
|
@@ -15,28 +13,14 @@ if ! id shortlinks >/dev/null 2>&1; then
|
|
|
15
13
|
useradd --system --create-home --home-dir "${SHORTLINKS_HOME}" --shell /sbin/nologin shortlinks
|
|
16
14
|
fi
|
|
17
15
|
|
|
18
|
-
install -d -o shortlinks -g shortlinks "${SHORTLINKS_HOME}/.hasna/cloud"
|
|
19
16
|
install -d -o shortlinks -g shortlinks "${SHORTLINKS_HOME}/.hasna/shortlinks"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"password_env": "HASNA_RDS_PASSWORD",
|
|
28
|
-
"ssl": true
|
|
29
|
-
},
|
|
30
|
-
"mode": "hybrid",
|
|
31
|
-
"feedback_endpoint": "https://feedback.hasna.com/api/v1/feedback",
|
|
32
|
-
"auto_sync_interval_minutes": 0,
|
|
33
|
-
"sync": {
|
|
34
|
-
"schedule_minutes": 0
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
CLOUD_CONFIG
|
|
38
|
-
chown shortlinks:shortlinks "${SHORTLINKS_HOME}/.hasna/cloud/config.json"
|
|
39
|
-
chmod 600 "${SHORTLINKS_HOME}/.hasna/cloud/config.json"
|
|
17
|
+
cat > /etc/shortlinks.env <<ENV
|
|
18
|
+
SHORTLINKS_DATABASE_SECRET_ID=${SHORTLINKS_DATABASE_SECRET_ID}
|
|
19
|
+
HASNA_SHORTLINKS_STORE=postgres
|
|
20
|
+
HASNA_SHORTLINKS_DATABASE_SSL=true
|
|
21
|
+
ENV
|
|
22
|
+
chown root:shortlinks /etc/shortlinks.env
|
|
23
|
+
chmod 640 /etc/shortlinks.env
|
|
40
24
|
|
|
41
25
|
su -s /bin/bash shortlinks -c 'curl -fsSL https://bun.sh/install | bash'
|
|
42
26
|
su -s /bin/bash shortlinks -c "${SHORTLINKS_HOME}/.bun/bin/bun install -g ${SHORTLINKS_PACKAGE} --no-cache"
|
|
@@ -48,16 +32,32 @@ set -euo pipefail
|
|
|
48
32
|
export AWS_REGION="${AWS_REGION:-us-east-1}"
|
|
49
33
|
export HOME="/var/lib/shortlinks"
|
|
50
34
|
export PATH="/var/lib/shortlinks/.bun/bin:/usr/local/bin:/usr/bin:/bin"
|
|
51
|
-
export
|
|
35
|
+
export HASNA_SHORTLINKS_STORE="postgres"
|
|
36
|
+
export HASNA_SHORTLINKS_DATABASE_SSL="${HASNA_SHORTLINKS_DATABASE_SSL:-true}"
|
|
52
37
|
|
|
53
38
|
secret_json="$(aws secretsmanager get-secret-value \
|
|
54
39
|
--region "${AWS_REGION}" \
|
|
55
|
-
--secret-id "
|
|
40
|
+
--secret-id "${SHORTLINKS_DATABASE_SECRET_ID:-hasna/xyz/opensource/shortlinks/prod/postgres}" \
|
|
56
41
|
--query SecretString \
|
|
57
42
|
--output text)"
|
|
58
43
|
|
|
59
|
-
|
|
60
|
-
|
|
44
|
+
connection_url="$(jq -r '(.connectionString // .connection_string // .url // .database_url // empty)' <<<"${secret_json}")"
|
|
45
|
+
if [[ -z "${connection_url}" ]]; then
|
|
46
|
+
db_host="$(jq -r '.host // empty' <<<"${secret_json}")"
|
|
47
|
+
db_port="$(jq -r '.port // 5432' <<<"${secret_json}")"
|
|
48
|
+
db_name="$(jq -r '.database // .dbname // "shortlinks"' <<<"${secret_json}")"
|
|
49
|
+
db_user="$(jq -r '.username // .user // empty' <<<"${secret_json}")"
|
|
50
|
+
db_password="$(jq -r '.password // empty' <<<"${secret_json}")"
|
|
51
|
+
if [[ -z "${db_host}" || -z "${db_user}" || -z "${db_password}" ]]; then
|
|
52
|
+
echo "Shortlinks database secret must include connectionString/url or host, username, and password." >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
db_user_encoded="$(jq -nr --arg value "${db_user}" '$value|@uri')"
|
|
56
|
+
db_password_encoded="$(jq -nr --arg value "${db_password}" '$value|@uri')"
|
|
57
|
+
connection_url="postgres://${db_user_encoded}:${db_password_encoded}@${db_host}:${db_port}/${db_name}"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
export HASNA_SHORTLINKS_DATABASE_URL="${connection_url}"
|
|
61
61
|
|
|
62
62
|
exec "$@"
|
|
63
63
|
RUNNER
|
|
@@ -90,8 +90,9 @@ Group=shortlinks
|
|
|
90
90
|
WorkingDirectory=/var/lib/shortlinks
|
|
91
91
|
Environment=HOME=/var/lib/shortlinks
|
|
92
92
|
Environment=PATH=/var/lib/shortlinks/.bun/bin:/usr/local/bin:/usr/bin:/bin
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
EnvironmentFile=/etc/shortlinks.env
|
|
94
|
+
ExecStartPre=/usr/local/bin/shortlinks-env-exec shortlinks postgres migrate
|
|
95
|
+
ExecStart=/usr/local/bin/shortlinks-env-exec shortlinks --store postgres serve --host 127.0.0.1 --port 8787 --default-host has.na
|
|
95
96
|
Restart=always
|
|
96
97
|
RestartSec=5
|
|
97
98
|
|
|
@@ -133,4 +134,4 @@ CADDY
|
|
|
133
134
|
systemctl daemon-reload
|
|
134
135
|
systemctl enable shortlinks.service caddy.service
|
|
135
136
|
systemctl start shortlinks.service
|
|
136
|
-
systemctl start caddy.service
|
|
137
|
+
systemctl start caddy.service
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/shortlinks",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI-only shortlink manager for custom domains, click tracking, Cloudflare setup, and
|
|
3
|
+
"version": "0.1.23",
|
|
4
|
+
"description": "CLI-only shortlink manager for custom domains, click tracking, Cloudflare setup, and app-owned Postgres runtime support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
"./cloudflare": {
|
|
21
21
|
"types": "./dist/cloudflare.d.ts",
|
|
22
22
|
"import": "./dist/cloudflare.js"
|
|
23
|
+
},
|
|
24
|
+
"./postgres": {
|
|
25
|
+
"types": "./dist/pg-store.d.ts",
|
|
26
|
+
"import": "./dist/pg-store.js"
|
|
27
|
+
},
|
|
28
|
+
"./runtime": {
|
|
29
|
+
"types": "./dist/runtime.d.ts",
|
|
30
|
+
"import": "./dist/runtime.js"
|
|
23
31
|
}
|
|
24
32
|
},
|
|
25
33
|
"files": [
|
|
@@ -31,11 +39,11 @@
|
|
|
31
39
|
"SECURITY.md"
|
|
32
40
|
],
|
|
33
41
|
"scripts": {
|
|
34
|
-
"build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun
|
|
42
|
+
"build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun && bun build src/index.ts --outdir dist --target bun && bun build src/server.ts --outdir dist --target bun && bun build src/cloudflare.ts --outdir dist --target bun && bun build src/runtime.ts --outdir dist --target bun && bun build src/pg-store.ts --outdir dist --target bun && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
35
43
|
"typecheck": "tsc --noEmit",
|
|
36
44
|
"test": "bun test",
|
|
37
45
|
"dev:cli": "bun run src/cli/index.ts",
|
|
38
|
-
"prepublishOnly": "bun run test && bun run build"
|
|
46
|
+
"prepublishOnly": "bun run typecheck && bun run test && bun run build"
|
|
39
47
|
},
|
|
40
48
|
"keywords": [
|
|
41
49
|
"shortlinks",
|
|
@@ -66,12 +74,13 @@
|
|
|
66
74
|
"author": "Andrei Hasna <andrei@hasna.com>",
|
|
67
75
|
"license": "Apache-2.0",
|
|
68
76
|
"dependencies": {
|
|
69
|
-
"@hasna/cloud": "0.1.30",
|
|
70
77
|
"@hasna/events": "^0.1.6",
|
|
71
78
|
"chalk": "^5.4.1",
|
|
72
|
-
"commander": "^13.1.0"
|
|
79
|
+
"commander": "^13.1.0",
|
|
80
|
+
"pg": "^8.13.3"
|
|
73
81
|
},
|
|
74
82
|
"devDependencies": {
|
|
83
|
+
"@types/pg": "^8.11.11",
|
|
75
84
|
"@types/bun": "^1.2.4",
|
|
76
85
|
"typescript": "^5.7.3"
|
|
77
86
|
}
|