@hasna/shortlinks 0.1.5 → 0.1.7
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 +11 -0
- package/dist/cli/index.js +12 -2
- package/dist/index.js +12 -2
- package/dist/server.js +12 -2
- package/infra/aws-ec2-user-data.sh +168 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -138,6 +138,17 @@ shortlinks cloud sync
|
|
|
138
138
|
|
|
139
139
|
The cloud database service name is `shortlinks`.
|
|
140
140
|
|
|
141
|
+
## AWS Origin
|
|
142
|
+
|
|
143
|
+
For an apex domain that needs stable A records, `infra/aws-ec2-user-data.sh` bootstraps a small EC2 redirect origin with:
|
|
144
|
+
|
|
145
|
+
- `@hasna/shortlinks` installed through Bun
|
|
146
|
+
- local SQLite data synced with the `shortlinks` RDS database through `@hasna/cloud`
|
|
147
|
+
- Caddy terminating HTTPS and proxying to `shortlinks serve`
|
|
148
|
+
- a systemd timer that syncs links and clicks every minute
|
|
149
|
+
|
|
150
|
+
The script reads the RDS password from AWS Secrets Manager through the instance role; it does not contain secret values.
|
|
151
|
+
|
|
141
152
|
## Development
|
|
142
153
|
|
|
143
154
|
```bash
|
package/dist/cli/index.js
CHANGED
|
@@ -3128,13 +3128,23 @@ function createShortlinksHandler(options = {}) {
|
|
|
3128
3128
|
if (url.pathname === "/" || url.pathname === "") {
|
|
3129
3129
|
return json({ service: "shortlinks", ok: true });
|
|
3130
3130
|
}
|
|
3131
|
-
|
|
3131
|
+
let slug = "";
|
|
3132
|
+
try {
|
|
3133
|
+
slug = decodeURIComponent(url.pathname.replace(/^\/+/, "").split("/")[0] || "");
|
|
3134
|
+
} catch {
|
|
3135
|
+
return json({ error: "Invalid slug." }, 400);
|
|
3136
|
+
}
|
|
3132
3137
|
if (!slug)
|
|
3133
3138
|
return json({ error: "Missing slug." }, 404);
|
|
3134
3139
|
const host = getHost(request, options.defaultHost);
|
|
3135
3140
|
if (!host)
|
|
3136
3141
|
return json({ error: "Missing Host header." }, 400);
|
|
3137
|
-
|
|
3142
|
+
let link = null;
|
|
3143
|
+
try {
|
|
3144
|
+
link = store.resolve(host, slug);
|
|
3145
|
+
} catch {
|
|
3146
|
+
return json({ error: "Shortlink not found.", slug, host }, 404);
|
|
3147
|
+
}
|
|
3138
3148
|
if (!link)
|
|
3139
3149
|
return json({ error: "Shortlink not found.", slug, host }, 404);
|
|
3140
3150
|
if (!link.active)
|
package/dist/index.js
CHANGED
|
@@ -561,13 +561,23 @@ function createShortlinksHandler(options = {}) {
|
|
|
561
561
|
if (url.pathname === "/" || url.pathname === "") {
|
|
562
562
|
return json({ service: "shortlinks", ok: true });
|
|
563
563
|
}
|
|
564
|
-
|
|
564
|
+
let slug = "";
|
|
565
|
+
try {
|
|
566
|
+
slug = decodeURIComponent(url.pathname.replace(/^\/+/, "").split("/")[0] || "");
|
|
567
|
+
} catch {
|
|
568
|
+
return json({ error: "Invalid slug." }, 400);
|
|
569
|
+
}
|
|
565
570
|
if (!slug)
|
|
566
571
|
return json({ error: "Missing slug." }, 404);
|
|
567
572
|
const host = getHost(request, options.defaultHost);
|
|
568
573
|
if (!host)
|
|
569
574
|
return json({ error: "Missing Host header." }, 400);
|
|
570
|
-
|
|
575
|
+
let link = null;
|
|
576
|
+
try {
|
|
577
|
+
link = store.resolve(host, slug);
|
|
578
|
+
} catch {
|
|
579
|
+
return json({ error: "Shortlink not found.", slug, host }, 404);
|
|
580
|
+
}
|
|
571
581
|
if (!link)
|
|
572
582
|
return json({ error: "Shortlink not found.", slug, host }, 404);
|
|
573
583
|
if (!link.active)
|
package/dist/server.js
CHANGED
|
@@ -563,13 +563,23 @@ function createShortlinksHandler(options = {}) {
|
|
|
563
563
|
if (url.pathname === "/" || url.pathname === "") {
|
|
564
564
|
return json({ service: "shortlinks", ok: true });
|
|
565
565
|
}
|
|
566
|
-
|
|
566
|
+
let slug = "";
|
|
567
|
+
try {
|
|
568
|
+
slug = decodeURIComponent(url.pathname.replace(/^\/+/, "").split("/")[0] || "");
|
|
569
|
+
} catch {
|
|
570
|
+
return json({ error: "Invalid slug." }, 400);
|
|
571
|
+
}
|
|
567
572
|
if (!slug)
|
|
568
573
|
return json({ error: "Missing slug." }, 404);
|
|
569
574
|
const host = getHost(request, options.defaultHost);
|
|
570
575
|
if (!host)
|
|
571
576
|
return json({ error: "Missing Host header." }, 400);
|
|
572
|
-
|
|
577
|
+
let link = null;
|
|
578
|
+
try {
|
|
579
|
+
link = store.resolve(host, slug);
|
|
580
|
+
} catch {
|
|
581
|
+
return json({ error: "Shortlink not found.", slug, host }, 404);
|
|
582
|
+
}
|
|
573
583
|
if (!link)
|
|
574
584
|
return json({ error: "Shortlink not found.", slug, host }, 404);
|
|
575
585
|
if (!link.active)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
export AWS_REGION="${AWS_REGION:-us-east-1}"
|
|
5
|
+
export SHORTLINKS_HOME="/var/lib/shortlinks"
|
|
6
|
+
export SHORTLINKS_PACKAGE="@hasna/shortlinks@latest"
|
|
7
|
+
export RDS_SECRET_ID="rds!db-7a451ce6-83a9-40fa-b24a-81e5d5943511"
|
|
8
|
+
export RDS_HOST="hasnaxyz-prod-opensource.c4limg0qgqvk.us-east-1.rds.amazonaws.com"
|
|
9
|
+
export RDS_USERNAME="hasna_admin"
|
|
10
|
+
|
|
11
|
+
dnf update -y
|
|
12
|
+
dnf install -y awscli jq tar gzip shadow-utils libcap
|
|
13
|
+
|
|
14
|
+
if ! id shortlinks >/dev/null 2>&1; then
|
|
15
|
+
useradd --system --create-home --home-dir "${SHORTLINKS_HOME}" --shell /sbin/nologin shortlinks
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
install -d -o shortlinks -g shortlinks "${SHORTLINKS_HOME}/.hasna/cloud"
|
|
19
|
+
install -d -o shortlinks -g shortlinks "${SHORTLINKS_HOME}/.hasna/shortlinks"
|
|
20
|
+
|
|
21
|
+
cat > "${SHORTLINKS_HOME}/.hasna/cloud/config.json" <<CLOUD_CONFIG
|
|
22
|
+
{
|
|
23
|
+
"rds": {
|
|
24
|
+
"host": "${RDS_HOST}",
|
|
25
|
+
"port": 5432,
|
|
26
|
+
"username": "${RDS_USERNAME}",
|
|
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"
|
|
40
|
+
|
|
41
|
+
su -s /bin/bash shortlinks -c 'curl -fsSL https://bun.sh/install | bash'
|
|
42
|
+
su -s /bin/bash shortlinks -c "${SHORTLINKS_HOME}/.bun/bin/bun install -g ${SHORTLINKS_PACKAGE} --no-cache"
|
|
43
|
+
|
|
44
|
+
cat > /usr/local/bin/shortlinks-env-exec <<'RUNNER'
|
|
45
|
+
#!/usr/bin/env bash
|
|
46
|
+
set -euo pipefail
|
|
47
|
+
|
|
48
|
+
export AWS_REGION="${AWS_REGION:-us-east-1}"
|
|
49
|
+
export HOME="/var/lib/shortlinks"
|
|
50
|
+
export PATH="/var/lib/shortlinks/.bun/bin:/usr/local/bin:/usr/bin:/bin"
|
|
51
|
+
export NODE_TLS_REJECT_UNAUTHORIZED="0"
|
|
52
|
+
|
|
53
|
+
secret_json="$(aws secretsmanager get-secret-value \
|
|
54
|
+
--region "${AWS_REGION}" \
|
|
55
|
+
--secret-id "rds!db-7a451ce6-83a9-40fa-b24a-81e5d5943511" \
|
|
56
|
+
--query SecretString \
|
|
57
|
+
--output text)"
|
|
58
|
+
|
|
59
|
+
export HASNA_RDS_PASSWORD
|
|
60
|
+
HASNA_RDS_PASSWORD="$(jq -r '.password' <<<"${secret_json}")"
|
|
61
|
+
|
|
62
|
+
exec "$@"
|
|
63
|
+
RUNNER
|
|
64
|
+
chmod 750 /usr/local/bin/shortlinks-env-exec
|
|
65
|
+
chown root:shortlinks /usr/local/bin/shortlinks-env-exec
|
|
66
|
+
|
|
67
|
+
su -s /bin/bash shortlinks -c '/usr/local/bin/shortlinks-env-exec shortlinks --json doctor'
|
|
68
|
+
su -s /bin/bash shortlinks -c '/usr/local/bin/shortlinks-env-exec shortlinks --json config set default-domain has.na'
|
|
69
|
+
su -s /bin/bash shortlinks -c '/usr/local/bin/shortlinks-env-exec shortlinks --json cloud pull --tables domains,links,clicks' || true
|
|
70
|
+
|
|
71
|
+
caddy_version="$(curl -fsSL https://api.github.com/repos/caddyserver/caddy/releases/latest | jq -r '.tag_name // "v2.10.2"' | sed 's/^v//')"
|
|
72
|
+
case "$(uname -m)" in
|
|
73
|
+
aarch64|arm64) caddy_arch="arm64" ;;
|
|
74
|
+
x86_64|amd64) caddy_arch="amd64" ;;
|
|
75
|
+
*) caddy_arch="arm64" ;;
|
|
76
|
+
esac
|
|
77
|
+
curl -fsSL -o /tmp/caddy.tar.gz "https://github.com/caddyserver/caddy/releases/download/v${caddy_version}/caddy_${caddy_version}_linux_${caddy_arch}.tar.gz"
|
|
78
|
+
tar -xzf /tmp/caddy.tar.gz -C /tmp caddy
|
|
79
|
+
install -m 0755 /tmp/caddy /usr/local/bin/caddy
|
|
80
|
+
setcap cap_net_bind_service=+ep /usr/local/bin/caddy || true
|
|
81
|
+
|
|
82
|
+
cat > /etc/systemd/system/shortlinks.service <<'SERVICE'
|
|
83
|
+
[Unit]
|
|
84
|
+
Description=Shortlinks redirect server
|
|
85
|
+
After=network-online.target
|
|
86
|
+
Wants=network-online.target
|
|
87
|
+
|
|
88
|
+
[Service]
|
|
89
|
+
Type=simple
|
|
90
|
+
User=shortlinks
|
|
91
|
+
Group=shortlinks
|
|
92
|
+
WorkingDirectory=/var/lib/shortlinks
|
|
93
|
+
Environment=HOME=/var/lib/shortlinks
|
|
94
|
+
Environment=PATH=/var/lib/shortlinks/.bun/bin:/usr/local/bin:/usr/bin:/bin
|
|
95
|
+
ExecStartPre=/usr/local/bin/shortlinks-env-exec shortlinks --json cloud pull --tables domains,links
|
|
96
|
+
ExecStart=/usr/local/bin/shortlinks-env-exec shortlinks serve --host 127.0.0.1 --port 8787 --default-host has.na
|
|
97
|
+
Restart=always
|
|
98
|
+
RestartSec=5
|
|
99
|
+
|
|
100
|
+
[Install]
|
|
101
|
+
WantedBy=multi-user.target
|
|
102
|
+
SERVICE
|
|
103
|
+
|
|
104
|
+
cat > /etc/systemd/system/shortlinks-sync.service <<'SERVICE'
|
|
105
|
+
[Unit]
|
|
106
|
+
Description=Sync shortlinks SQLite data with RDS
|
|
107
|
+
After=network-online.target
|
|
108
|
+
Wants=network-online.target
|
|
109
|
+
|
|
110
|
+
[Service]
|
|
111
|
+
Type=oneshot
|
|
112
|
+
User=shortlinks
|
|
113
|
+
Group=shortlinks
|
|
114
|
+
WorkingDirectory=/var/lib/shortlinks
|
|
115
|
+
Environment=HOME=/var/lib/shortlinks
|
|
116
|
+
Environment=PATH=/var/lib/shortlinks/.bun/bin:/usr/local/bin:/usr/bin:/bin
|
|
117
|
+
ExecStart=/usr/local/bin/shortlinks-env-exec shortlinks --json cloud sync --tables domains,links,clicks
|
|
118
|
+
SERVICE
|
|
119
|
+
|
|
120
|
+
cat > /etc/systemd/system/shortlinks-sync.timer <<'TIMER'
|
|
121
|
+
[Unit]
|
|
122
|
+
Description=Run shortlinks cloud sync every minute
|
|
123
|
+
|
|
124
|
+
[Timer]
|
|
125
|
+
OnBootSec=2min
|
|
126
|
+
OnUnitActiveSec=1min
|
|
127
|
+
Unit=shortlinks-sync.service
|
|
128
|
+
|
|
129
|
+
[Install]
|
|
130
|
+
WantedBy=timers.target
|
|
131
|
+
TIMER
|
|
132
|
+
|
|
133
|
+
cat > /etc/systemd/system/caddy.service <<'SERVICE'
|
|
134
|
+
[Unit]
|
|
135
|
+
Description=Caddy web server for shortlinks
|
|
136
|
+
After=network-online.target shortlinks.service
|
|
137
|
+
Wants=network-online.target
|
|
138
|
+
|
|
139
|
+
[Service]
|
|
140
|
+
Type=simple
|
|
141
|
+
User=root
|
|
142
|
+
Group=root
|
|
143
|
+
ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile
|
|
144
|
+
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --force
|
|
145
|
+
TimeoutStopSec=5s
|
|
146
|
+
LimitNOFILE=1048576
|
|
147
|
+
PrivateTmp=true
|
|
148
|
+
ProtectSystem=full
|
|
149
|
+
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
150
|
+
Restart=on-failure
|
|
151
|
+
|
|
152
|
+
[Install]
|
|
153
|
+
WantedBy=multi-user.target
|
|
154
|
+
SERVICE
|
|
155
|
+
|
|
156
|
+
install -d /etc/caddy
|
|
157
|
+
cat > /etc/caddy/Caddyfile <<'CADDY'
|
|
158
|
+
has.na {
|
|
159
|
+
encode zstd gzip
|
|
160
|
+
reverse_proxy 127.0.0.1:8787
|
|
161
|
+
}
|
|
162
|
+
CADDY
|
|
163
|
+
|
|
164
|
+
systemctl daemon-reload
|
|
165
|
+
systemctl enable shortlinks.service shortlinks-sync.timer caddy.service
|
|
166
|
+
systemctl start shortlinks.service
|
|
167
|
+
systemctl start shortlinks-sync.timer
|
|
168
|
+
systemctl start caddy.service || true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/shortlinks",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
27
|
"cloudflare",
|
|
28
|
+
"infra",
|
|
28
29
|
"LICENSE",
|
|
29
30
|
"README.md",
|
|
30
31
|
"SECURITY.md"
|