codex-snapshots 0.1.0 → 0.1.1
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 +101 -6
- package/bin/codex-snapshot.mjs +1 -6326
- package/deploy/aliyun/README.md +311 -0
- package/deploy/aliyun/backup-share-data.sh +109 -0
- package/deploy/aliyun/check-ecs-status.sh +149 -0
- package/deploy/aliyun/codex-snapshot-share.env.example +29 -0
- package/deploy/aliyun/codex-snapshot-share.service +26 -0
- package/deploy/aliyun/configure-github-pages-api.sh +141 -0
- package/deploy/aliyun/configure-local-publisher.sh +197 -0
- package/deploy/aliyun/deploy-to-ecs.sh +669 -0
- package/deploy/aliyun/deploy.env.example +52 -0
- package/deploy/aliyun/doctor.mjs +398 -0
- package/deploy/aliyun/install-share-api.sh +252 -0
- package/deploy/aliyun/install-system-deps.sh +84 -0
- package/deploy/aliyun/nginx-codex-snapshots.bootstrap.conf +34 -0
- package/deploy/aliyun/nginx-codex-snapshots.conf +52 -0
- package/deploy/aliyun/preflight.mjs +321 -0
- package/deploy/aliyun/restore-share-data.sh +141 -0
- package/deploy/aliyun/verify-public-share.mjs +404 -0
- package/dist/cli/codex-snapshot.mjs +2654 -0
- package/dist/core/privacy.js +81 -0
- package/dist/core/snapshot.js +1 -0
- package/dist/renderers/markdown.mjs +81 -0
- package/dist/renderers/transcript.js +195 -0
- package/dist/server/http.js +10 -0
- package/dist/server/local-security.js +66 -0
- package/dist/server/local-viewer-app.mjs +1670 -0
- package/dist/server/local-viewer.mjs +210 -0
- package/dist/server/share-api.mjs +1149 -0
- package/dist/server/share-store.js +136 -0
- package/dist/shared/sanitize.js +126 -0
- package/dist/shared/transcript.js +1 -0
- package/dist/sources/index.mjs +2 -0
- package/dist/sources/local-history.mjs +2221 -0
- package/package.json +42 -14
- package/scripts/build-site.mjs +71 -0
- package/scripts/launch-agent.mjs +19 -227
- package/scripts/serve-site.mjs +2 -2
- package/scripts/test-aliyun-deploy-config.sh +230 -0
- package/scripts/test-share-api.mjs +967 -0
- package/scripts/test-site-config.mjs +100 -0
- package/scripts/test-static-site.mjs +403 -0
- package/scripts/write-site-config.mjs +161 -0
- package/server/share-api.mjs +1 -771
- package/site/assets/config.js +3 -0
- package/site/assets/share.js +43 -106
- package/site/assets/site.css +3 -605
- package/site/assets/site.js +15 -92
- package/site/favicon.svg +7 -0
- package/site/index.html +3 -83
- package/site/share/index.html +3 -8
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# Deploy the Share API on Alibaba Cloud ECS
|
|
2
|
+
|
|
3
|
+
This deployment runs `codex-snapshot-share` on `127.0.0.1:8787`, exposes it through Nginx + HTTPS, and lets the GitHub Pages site list and render public sessions.
|
|
4
|
+
|
|
5
|
+
Replace these examples before running commands:
|
|
6
|
+
|
|
7
|
+
- `snapshots.example.com`: your public API domain
|
|
8
|
+
- `your-client-id` / `your-client-secret`: your GitHub OAuth App credentials
|
|
9
|
+
- `your-github-login`: your GitHub login; this account can delete any shared session
|
|
10
|
+
- `https://ffffhx.github.io/codex-snapshots/`: the public static site
|
|
11
|
+
|
|
12
|
+
## 1. Prepare Alibaba Cloud
|
|
13
|
+
|
|
14
|
+
1. Point a DNS `A` record for `snapshots.example.com` to the ECS public IP.
|
|
15
|
+
2. In the ECS security group, allow inbound TCP `80` and `443`.
|
|
16
|
+
3. Do not expose port `8787` publicly; the service binds to `127.0.0.1`.
|
|
17
|
+
|
|
18
|
+
## 2. Install host dependencies
|
|
19
|
+
|
|
20
|
+
On the ECS host, install Node.js 18 or newer, Nginx, Certbot, Git, OpenSSL, and rsync.
|
|
21
|
+
|
|
22
|
+
Ubuntu/Debian example:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
sudo apt-get update
|
|
26
|
+
sudo apt-get install -y nginx certbot python3-certbot-nginx git openssl rsync
|
|
27
|
+
node --version
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Alibaba Cloud Linux/RHEL-style systems usually use `dnf` or `yum` packages instead of `apt`.
|
|
31
|
+
|
|
32
|
+
You can also let the deploy helper install the common dependencies on Ubuntu/Debian, Alibaba Cloud Linux, or RHEL-style hosts:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
deploy/aliyun/deploy-to-ecs.sh \
|
|
36
|
+
--ssh root@1.2.3.4 \
|
|
37
|
+
--domain snapshots.example.com \
|
|
38
|
+
--install-deps \
|
|
39
|
+
--configure-local
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The helper installs Node.js 20 when the host does not already have Node.js 18+.
|
|
43
|
+
|
|
44
|
+
## 3. Deploy from your local machine
|
|
45
|
+
|
|
46
|
+
Optionally create a local deployment config so you do not have to type the long command each time:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cp deploy/aliyun/deploy.env.example deploy/aliyun/deploy.env
|
|
50
|
+
$EDITOR deploy/aliyun/deploy.env
|
|
51
|
+
deploy/aliyun/deploy-to-ecs.sh --config deploy/aliyun/deploy.env --dry-run
|
|
52
|
+
node deploy/aliyun/doctor.mjs --config deploy/aliyun/deploy.env
|
|
53
|
+
deploy/aliyun/deploy-to-ecs.sh --config deploy/aliyun/deploy.env
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`deploy/aliyun/deploy.env` is ignored by git because it contains host details, OAuth secrets, and optional legacy token values.
|
|
57
|
+
The dry run validates that you replaced template values like `root@1.2.3.4`, `snapshots.example.com`, `change-me`, and `you@example.com` before anything connects to ECS.
|
|
58
|
+
The doctor command adds local readiness checks for DNS, local `ssh`/`rsync`/`node`, GitHub CLI when Pages auto-config is enabled, auth config, and the deploy dry run. Pass `--offline` if DNS has not propagated yet.
|
|
59
|
+
The deploy and install scripts also exclude local secret files such as `.env`, `deploy/aliyun/deploy.env`, private-key files, and local `backups/` from the ECS rsync payload.
|
|
60
|
+
|
|
61
|
+
Run the preflight check before deploying:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
node deploy/aliyun/preflight.mjs \
|
|
65
|
+
--domain snapshots.example.com \
|
|
66
|
+
--ssh root@1.2.3.4
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This checks DNS, SSH access, Node.js 18+, Nginx, Certbot, rsync, OpenSSL, systemd, and root/passwordless-sudo privileges on the ECS host.
|
|
70
|
+
When you deploy with `--issue-cert`, the deploy helper also checks that Certbot has the Nginx plugin.
|
|
71
|
+
|
|
72
|
+
After DNS points at the ECS public IP and SSH works, deploy the current checkout from your local machine:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
SNAPSHOT_GITHUB_CLIENT_ID=your-client-id \
|
|
76
|
+
SNAPSHOT_GITHUB_CLIENT_SECRET=your-client-secret \
|
|
77
|
+
SNAPSHOT_SESSION_SECRET="$(openssl rand -base64 48)" \
|
|
78
|
+
SNAPSHOT_GITHUB_OWNER_LOGIN=your-github-login \
|
|
79
|
+
deploy/aliyun/deploy-to-ecs.sh \
|
|
80
|
+
--ssh root@1.2.3.4 \
|
|
81
|
+
--domain snapshots.example.com \
|
|
82
|
+
--configure-local
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This runs the preflight checks, rsyncs the repository to the ECS host, installs the systemd service, and writes the Nginx bootstrap config. If you have already checked the host and want to skip preflight, pass `--no-preflight`.
|
|
86
|
+
GitHub OAuth makes publish/delete use GitHub login instead of a shared token. The site owner in `SNAPSHOT_GITHUB_OWNER_LOGIN` can delete any shared session; other GitHub users can delete only their own sessions.
|
|
87
|
+
Keep the same `SNAPSHOT_SESSION_SECRET` across redeploys so existing login cookies remain valid. If you omit it while OAuth vars are present, the deploy helper generates one for that deploy.
|
|
88
|
+
|
|
89
|
+
After port `80` works, issue HTTPS and switch to the HTTPS proxy template:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
deploy/aliyun/deploy-to-ecs.sh \
|
|
93
|
+
--ssh root@1.2.3.4 \
|
|
94
|
+
--domain snapshots.example.com \
|
|
95
|
+
--issue-cert \
|
|
96
|
+
--email you@example.com
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The deploy script verifies the ECS API at the end. In GitHub OAuth mode it skips command-line token publishing because writes require a browser GitHub login; configure Pages in step 6, then run the public loop verification in step 7.
|
|
100
|
+
|
|
101
|
+
You can also let the deploy command configure GitHub Pages and the local publisher in the same run:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
deploy/aliyun/deploy-to-ecs.sh \
|
|
105
|
+
--ssh root@1.2.3.4 \
|
|
106
|
+
--domain snapshots.example.com \
|
|
107
|
+
--install-deps \
|
|
108
|
+
--issue-cert \
|
|
109
|
+
--email you@example.com \
|
|
110
|
+
--configure-pages \
|
|
111
|
+
--wait-pages \
|
|
112
|
+
--configure-local \
|
|
113
|
+
--reinstall-daemon
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`--configure-pages` requires GitHub CLI auth on your local machine. With `--wait-pages`, the deploy script waits for the Pages workflow and then runs the full public verification.
|
|
117
|
+
|
|
118
|
+
## 4. Install the service manually
|
|
119
|
+
|
|
120
|
+
Clone this repository on the ECS host, then run the installer from the repository root:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
git clone https://github.com/ffffhx/codex-snapshots.git
|
|
124
|
+
cd codex-snapshots
|
|
125
|
+
|
|
126
|
+
sudo DOMAIN=snapshots.example.com \
|
|
127
|
+
SNAPSHOT_GITHUB_CLIENT_ID=your-client-id \
|
|
128
|
+
SNAPSHOT_GITHUB_CLIENT_SECRET=your-client-secret \
|
|
129
|
+
SNAPSHOT_SESSION_SECRET="$(openssl rand -base64 48)" \
|
|
130
|
+
SNAPSHOT_GITHUB_OWNER_LOGIN=your-github-login \
|
|
131
|
+
SNAPSHOT_SHARE_SITE_URL=https://ffffhx.github.io/codex-snapshots/ \
|
|
132
|
+
SNAPSHOT_SHARE_PUBLIC_API_URL=https://snapshots.example.com \
|
|
133
|
+
deploy/aliyun/install-share-api.sh
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The first run installs an HTTP bootstrap Nginx config if a TLS certificate is not present yet.
|
|
137
|
+
|
|
138
|
+
## 5. Enable HTTPS manually
|
|
139
|
+
|
|
140
|
+
Issue a certificate after DNS and port `80` are working:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
sudo certbot --nginx -d snapshots.example.com
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Then re-run the installer so it switches Nginx to the committed HTTPS proxy template:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
sudo DOMAIN=snapshots.example.com \
|
|
150
|
+
SNAPSHOT_GITHUB_CLIENT_ID=your-client-id \
|
|
151
|
+
SNAPSHOT_GITHUB_CLIENT_SECRET=your-client-secret \
|
|
152
|
+
SNAPSHOT_SESSION_SECRET="$(openssl rand -base64 48)" \
|
|
153
|
+
SNAPSHOT_GITHUB_OWNER_LOGIN=your-github-login \
|
|
154
|
+
SNAPSHOT_SHARE_SITE_URL=https://ffffhx.github.io/codex-snapshots/ \
|
|
155
|
+
SNAPSHOT_SHARE_PUBLIC_API_URL=https://snapshots.example.com \
|
|
156
|
+
deploy/aliyun/install-share-api.sh
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Verify:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
curl https://snapshots.example.com/api/snapshots/health
|
|
163
|
+
curl https://snapshots.example.com/api/snapshots
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## 6. Connect GitHub Pages
|
|
167
|
+
|
|
168
|
+
Set this repository variable in GitHub:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
CODEX_SNAPSHOTS_PUBLIC_API_URL=https://snapshots.example.com
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
You can set it and trigger the Pages workflow with GitHub CLI:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
deploy/aliyun/configure-github-pages-api.sh \
|
|
178
|
+
--api-url https://snapshots.example.com \
|
|
179
|
+
--repo ffffhx/codex-snapshots
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The helper refuses placeholder or local-only API URLs so the public site does not get configured to fetch from `127.0.0.1` or `snapshots.example.com`.
|
|
183
|
+
The Pages workflow runs the same validation before writing `site/assets/config.js`; empty is allowed, but invalid, example, localhost, and private-network API URLs fail the deploy step.
|
|
184
|
+
|
|
185
|
+
The Pages workflow writes `site/assets/config.js`, so the public homepage fetches:
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
https://snapshots.example.com/api/snapshots
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If this variable is missing, the public site shows that the share API is not configured instead of attempting to fetch from each visitor's `127.0.0.1`.
|
|
192
|
+
|
|
193
|
+
## 7. Verify the public loop
|
|
194
|
+
|
|
195
|
+
After the Pages workflow finishes, run the read-only checks:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
node deploy/aliyun/verify-public-share.mjs \
|
|
199
|
+
--api-url https://snapshots.example.com \
|
|
200
|
+
--site-url https://ffffhx.github.io/codex-snapshots/
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
This proves:
|
|
204
|
+
|
|
205
|
+
- the ECS API is reachable over HTTPS
|
|
206
|
+
- public reads work
|
|
207
|
+
- CORS allows the public GitHub Pages site
|
|
208
|
+
- the static site config points at the ECS API
|
|
209
|
+
|
|
210
|
+
GitHub OAuth publishing is verified from the browser: open the local viewer, click “发布分享”, log in with GitHub when prompted, and confirm the shared session appears on the public site. For legacy token mode only, add `--publish --token <same-token>` to create one small verification snapshot from the command line.
|
|
211
|
+
|
|
212
|
+
To verify only the ECS API before Pages has deployed, add `--skip-site-config`.
|
|
213
|
+
After configuring the local publisher, add `--check-local-config` to prove that `~/.codex-snapshots-agent.json` also points at the same Aliyun API and public site.
|
|
214
|
+
|
|
215
|
+
## 8. Publish from your local viewer
|
|
216
|
+
|
|
217
|
+
Configure your local viewer with the public API and site:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
SNAPSHOT_SHARE_API_URL=https://snapshots.example.com \
|
|
221
|
+
SNAPSHOT_SHARE_SITE_URL=https://ffffhx.github.io/codex-snapshots/ \
|
|
222
|
+
deploy/aliyun/configure-local-publisher.sh
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The helper rejects placeholder and local-only API URLs before it writes `~/.codex-snapshots-agent.json`.
|
|
226
|
+
That file stores the public API/site URLs, so the local viewer knows which remote share API to use. With GitHub OAuth, the browser session cookie handles publish/delete auth after login.
|
|
227
|
+
|
|
228
|
+
Verify the local publisher config:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
node deploy/aliyun/verify-public-share.mjs \
|
|
232
|
+
--api-url https://snapshots.example.com \
|
|
233
|
+
--site-url https://ffffhx.github.io/codex-snapshots/ \
|
|
234
|
+
--skip-site-config \
|
|
235
|
+
--check-local-config
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Then start the local viewer:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pnpm snapshot serve --port 4321
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
For the macOS LaunchAgent, let the helper reinstall the daemon with the public API and site URLs:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
SNAPSHOT_SHARE_API_URL=https://snapshots.example.com \
|
|
248
|
+
SNAPSHOT_SHARE_SITE_URL=https://ffffhx.github.io/codex-snapshots/ \
|
|
249
|
+
deploy/aliyun/configure-local-publisher.sh --reinstall-daemon
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The local publisher config can also be edited manually in `~/.codex-snapshots-agent.json`:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"snapshotShareApiUrl": "https://snapshots.example.com",
|
|
257
|
+
"snapshotShareSiteUrl": "https://ffffhx.github.io/codex-snapshots"
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Operations
|
|
262
|
+
|
|
263
|
+
Useful ECS commands:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
sudo systemctl status codex-snapshot-share
|
|
267
|
+
sudo journalctl -u codex-snapshot-share -f
|
|
268
|
+
sudo nginx -t
|
|
269
|
+
sudo systemctl reload nginx
|
|
270
|
+
sudo cp /var/lib/codex-snapshots/shares.json ~/shares.json.backup
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
You can run the same health checks from your local machine:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
deploy/aliyun/check-ecs-status.sh \
|
|
277
|
+
--ssh root@1.2.3.4 \
|
|
278
|
+
--domain snapshots.example.com
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
This checks systemd status, local health, install-file permissions, that port `8787` is only listening locally, Nginx syntax/config, and the public `/api/snapshots/health` endpoint.
|
|
282
|
+
|
|
283
|
+
Back up the share data from ECS to your local checkout:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
deploy/aliyun/backup-share-data.sh \
|
|
287
|
+
--ssh root@1.2.3.4
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Restore a local backup to ECS:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
deploy/aliyun/restore-share-data.sh \
|
|
294
|
+
--ssh root@1.2.3.4 \
|
|
295
|
+
--file backups/codex-snapshots/shares-20260101T000000Z.json
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The restore script validates the JSON locally and saves the previous remote file next to the live data before replacing it.
|
|
299
|
+
|
|
300
|
+
The share data is stored in:
|
|
301
|
+
|
|
302
|
+
```text
|
|
303
|
+
/var/lib/codex-snapshots/shares.json
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Local publisher checks:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
curl https://snapshots.example.com/api/snapshots/health
|
|
310
|
+
curl https://snapshots.example.com/api/snapshots
|
|
311
|
+
```
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SSH_TARGET=""
|
|
5
|
+
SSH_OPTS=()
|
|
6
|
+
REMOTE_FILE="/var/lib/codex-snapshots/shares.json"
|
|
7
|
+
OUTPUT_DIR="backups/codex-snapshots"
|
|
8
|
+
|
|
9
|
+
usage() {
|
|
10
|
+
cat <<'EOF'
|
|
11
|
+
Usage:
|
|
12
|
+
deploy/aliyun/backup-share-data.sh --ssh root@1.2.3.4
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--ssh TARGET SSH target, for example root@1.2.3.4.
|
|
16
|
+
--identity-file FILE SSH private key for the ECS host.
|
|
17
|
+
--port PORT SSH port.
|
|
18
|
+
--remote-file FILE Remote share data file. Defaults to /var/lib/codex-snapshots/shares.json.
|
|
19
|
+
--output-dir DIR Local backup directory. Defaults to backups/codex-snapshots.
|
|
20
|
+
-h, --help Show help.
|
|
21
|
+
EOF
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
while [[ $# -gt 0 ]]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--ssh)
|
|
27
|
+
SSH_TARGET="${2:-}"
|
|
28
|
+
shift 2
|
|
29
|
+
;;
|
|
30
|
+
--identity-file)
|
|
31
|
+
SSH_OPTS+=("-i" "${2:-}")
|
|
32
|
+
shift 2
|
|
33
|
+
;;
|
|
34
|
+
--port)
|
|
35
|
+
SSH_OPTS+=("-p" "${2:-}")
|
|
36
|
+
shift 2
|
|
37
|
+
;;
|
|
38
|
+
--remote-file)
|
|
39
|
+
REMOTE_FILE="${2:-}"
|
|
40
|
+
shift 2
|
|
41
|
+
;;
|
|
42
|
+
--output-dir)
|
|
43
|
+
OUTPUT_DIR="${2:-}"
|
|
44
|
+
shift 2
|
|
45
|
+
;;
|
|
46
|
+
-h|--help)
|
|
47
|
+
usage
|
|
48
|
+
exit 0
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
echo "Unknown option: $1" >&2
|
|
52
|
+
usage >&2
|
|
53
|
+
exit 1
|
|
54
|
+
;;
|
|
55
|
+
esac
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
if [[ -z "${SSH_TARGET}" ]]; then
|
|
59
|
+
echo "Missing --ssh target." >&2
|
|
60
|
+
usage >&2
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
shell_quote() {
|
|
65
|
+
printf "'"
|
|
66
|
+
printf "%s" "$1" | sed "s/'/'\"'\"'/g"
|
|
67
|
+
printf "'"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
mkdir -p "${OUTPUT_DIR}"
|
|
71
|
+
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
72
|
+
output_file="${OUTPUT_DIR}/shares-${timestamp}.json"
|
|
73
|
+
tmp_file="${output_file}.tmp"
|
|
74
|
+
remote_file_q="$(shell_quote "${REMOTE_FILE}")"
|
|
75
|
+
|
|
76
|
+
cleanup() {
|
|
77
|
+
rm -f "${tmp_file}"
|
|
78
|
+
}
|
|
79
|
+
trap cleanup EXIT
|
|
80
|
+
|
|
81
|
+
remote_cmd=$(
|
|
82
|
+
cat <<EOF
|
|
83
|
+
set -e
|
|
84
|
+
if [ "\$(id -u)" -eq 0 ]; then
|
|
85
|
+
cat ${remote_file_q}
|
|
86
|
+
else
|
|
87
|
+
sudo -n cat ${remote_file_q}
|
|
88
|
+
fi
|
|
89
|
+
EOF
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
SSH_CMD=(ssh)
|
|
93
|
+
if ((${#SSH_OPTS[@]})); then
|
|
94
|
+
SSH_CMD+=("${SSH_OPTS[@]}")
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
"${SSH_CMD[@]}" "${SSH_TARGET}" "${remote_cmd}" > "${tmp_file}"
|
|
98
|
+
|
|
99
|
+
node -e '
|
|
100
|
+
const { readFileSync } = require("node:fs");
|
|
101
|
+
const file = process.argv[1];
|
|
102
|
+
JSON.parse(readFileSync(file, "utf8"));
|
|
103
|
+
' "${tmp_file}"
|
|
104
|
+
|
|
105
|
+
mv "${tmp_file}" "${output_file}"
|
|
106
|
+
chmod 0600 "${output_file}"
|
|
107
|
+
trap - EXIT
|
|
108
|
+
|
|
109
|
+
echo "Backed up share data to ${output_file}"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SSH_TARGET=""
|
|
5
|
+
SSH_OPTS=()
|
|
6
|
+
DOMAIN=""
|
|
7
|
+
API_URL="${SNAPSHOT_SHARE_PUBLIC_API_URL:-${SNAPSHOT_SHARE_API_URL:-}}"
|
|
8
|
+
|
|
9
|
+
usage() {
|
|
10
|
+
cat <<'EOF'
|
|
11
|
+
Usage:
|
|
12
|
+
deploy/aliyun/check-ecs-status.sh --ssh root@1.2.3.4 --domain snapshots.example.com
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--ssh TARGET SSH target, for example root@1.2.3.4.
|
|
16
|
+
--domain DOMAIN Public API domain. Used to check https://DOMAIN/api/snapshots/health.
|
|
17
|
+
--api-url URL Public API URL. Overrides --domain for public health check.
|
|
18
|
+
--identity-file FILE SSH private key for the ECS host.
|
|
19
|
+
--port PORT SSH port.
|
|
20
|
+
-h, --help Show help.
|
|
21
|
+
EOF
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
while [[ $# -gt 0 ]]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--ssh)
|
|
27
|
+
SSH_TARGET="${2:-}"
|
|
28
|
+
shift 2
|
|
29
|
+
;;
|
|
30
|
+
--domain)
|
|
31
|
+
DOMAIN="${2:-}"
|
|
32
|
+
shift 2
|
|
33
|
+
;;
|
|
34
|
+
--api-url)
|
|
35
|
+
API_URL="${2:-}"
|
|
36
|
+
shift 2
|
|
37
|
+
;;
|
|
38
|
+
--identity-file)
|
|
39
|
+
SSH_OPTS+=("-i" "${2:-}")
|
|
40
|
+
shift 2
|
|
41
|
+
;;
|
|
42
|
+
--port)
|
|
43
|
+
SSH_OPTS+=("-p" "${2:-}")
|
|
44
|
+
shift 2
|
|
45
|
+
;;
|
|
46
|
+
-h|--help)
|
|
47
|
+
usage
|
|
48
|
+
exit 0
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
echo "Unknown option: $1" >&2
|
|
52
|
+
usage >&2
|
|
53
|
+
exit 1
|
|
54
|
+
;;
|
|
55
|
+
esac
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
if [[ -z "${SSH_TARGET}" ]]; then
|
|
59
|
+
echo "Missing --ssh target." >&2
|
|
60
|
+
usage >&2
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
if [[ -z "${API_URL}" && -n "${DOMAIN}" ]]; then
|
|
65
|
+
API_URL="https://${DOMAIN}"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
remote_cmd='
|
|
69
|
+
set -e
|
|
70
|
+
PATH="$PATH:/usr/local/sbin:/usr/sbin:/sbin"
|
|
71
|
+
|
|
72
|
+
run_root() {
|
|
73
|
+
if [ "$(id -u)" -eq 0 ]; then
|
|
74
|
+
"$@"
|
|
75
|
+
else
|
|
76
|
+
sudo -n "$@"
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
echo "== systemd =="
|
|
81
|
+
systemctl is-enabled codex-snapshot-share.service || true
|
|
82
|
+
systemctl is-active codex-snapshot-share.service || true
|
|
83
|
+
systemctl --no-pager --lines=12 status codex-snapshot-share.service || true
|
|
84
|
+
|
|
85
|
+
echo
|
|
86
|
+
echo "== local health =="
|
|
87
|
+
curl --fail --show-error --silent --max-time 8 http://127.0.0.1:8787/api/snapshots/health
|
|
88
|
+
echo
|
|
89
|
+
|
|
90
|
+
echo
|
|
91
|
+
echo "== install files =="
|
|
92
|
+
run_root test -f /etc/codex-snapshots/share-api.env
|
|
93
|
+
run_root test -f /etc/systemd/system/codex-snapshot-share.service
|
|
94
|
+
run_root test -d /var/lib/codex-snapshots
|
|
95
|
+
run_root test -f /etc/nginx/conf.d/codex-snapshots.conf
|
|
96
|
+
if command -v stat >/dev/null 2>&1; then
|
|
97
|
+
run_root stat -c "env %a %U:%G %n" /etc/codex-snapshots/share-api.env
|
|
98
|
+
run_root stat -c "state %a %U:%G %n" /var/lib/codex-snapshots
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
echo
|
|
102
|
+
echo "== local bind =="
|
|
103
|
+
if command -v ss >/dev/null 2>&1; then
|
|
104
|
+
ss -ltn | grep ":8787" || {
|
|
105
|
+
echo "share API is not listening on port 8787" >&2
|
|
106
|
+
exit 21
|
|
107
|
+
}
|
|
108
|
+
if ss -ltn | grep ":8787" | grep -Eq "0\\.0\\.0\\.0:8787|\\[::\\]:8787|\\*:8787"; then
|
|
109
|
+
echo "share API port 8787 is exposed beyond localhost" >&2
|
|
110
|
+
exit 22
|
|
111
|
+
fi
|
|
112
|
+
else
|
|
113
|
+
echo "ss command not found; skipped bind-scope check"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
echo
|
|
117
|
+
echo "== nginx =="
|
|
118
|
+
run_root nginx -t
|
|
119
|
+
run_root grep -q "proxy_pass http://127.0.0.1:8787" /etc/nginx/conf.d/codex-snapshots.conf
|
|
120
|
+
'
|
|
121
|
+
|
|
122
|
+
SSH_CMD=(ssh)
|
|
123
|
+
if ((${#SSH_OPTS[@]})); then
|
|
124
|
+
SSH_CMD+=("${SSH_OPTS[@]}")
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
"${SSH_CMD[@]}" "${SSH_TARGET}" "${remote_cmd}"
|
|
128
|
+
|
|
129
|
+
if [[ -n "${API_URL}" ]]; then
|
|
130
|
+
echo
|
|
131
|
+
echo "== public health =="
|
|
132
|
+
HEALTH_PAYLOAD="$(curl --fail --show-error --silent --max-time 12 "${API_URL%/}/api/snapshots/health")"
|
|
133
|
+
printf "%s\n" "${HEALTH_PAYLOAD}"
|
|
134
|
+
if printf "%s" "${HEALTH_PAYLOAD}" | grep -q '"storage"'; then
|
|
135
|
+
echo "public health response exposes the storage path" >&2
|
|
136
|
+
exit 31
|
|
137
|
+
fi
|
|
138
|
+
echo
|
|
139
|
+
|
|
140
|
+
echo "== public CORS =="
|
|
141
|
+
CORS_HEADERS="$(curl --fail --show-error --silent --max-time 12 -D - -o /dev/null \
|
|
142
|
+
-H "Origin: https://ffffhx.github.io" \
|
|
143
|
+
"${API_URL%/}/api/snapshots?limit=1")"
|
|
144
|
+
printf "%s\n" "${CORS_HEADERS}" | grep -qi '^access-control-allow-origin: \*' || {
|
|
145
|
+
echo "public list response is missing access-control-allow-origin: *" >&2
|
|
146
|
+
exit 32
|
|
147
|
+
}
|
|
148
|
+
echo "CORS read check passed."
|
|
149
|
+
fi
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copy to /etc/codex-snapshots/share-api.env on the ECS host.
|
|
2
|
+
# Generate a strong value, for example:
|
|
3
|
+
# openssl rand -base64 32
|
|
4
|
+
SNAPSHOT_SHARE_TOKEN=change-me
|
|
5
|
+
|
|
6
|
+
# Public GitHub Pages site that should render shared sessions.
|
|
7
|
+
SNAPSHOT_SHARE_SITE_URL=https://ffffhx.github.io/codex-snapshots/
|
|
8
|
+
|
|
9
|
+
# Public HTTPS API origin served by Nginx on the ECS host.
|
|
10
|
+
SNAPSHOT_SHARE_PUBLIC_API_URL=https://snapshots.example.com
|
|
11
|
+
|
|
12
|
+
# The public site path for the static share viewer.
|
|
13
|
+
SNAPSHOT_SHARE_VIEWER_PATH=/share/
|
|
14
|
+
|
|
15
|
+
# Persistent storage on the ECS host. Back this file up.
|
|
16
|
+
SNAPSHOT_SHARE_DATA_FILE=/var/lib/codex-snapshots/shares.json
|
|
17
|
+
|
|
18
|
+
# Keep writes/deletes authenticated. Public reads remain available.
|
|
19
|
+
SNAPSHOT_SHARE_ALLOW_ANONYMOUS=false
|
|
20
|
+
|
|
21
|
+
# Optional GitHub OAuth publish/delete auth.
|
|
22
|
+
# GitHub OAuth App callback URL:
|
|
23
|
+
# https://snapshots.example.com/api/auth/github/callback
|
|
24
|
+
# SNAPSHOT_GITHUB_CLIENT_ID=change-me
|
|
25
|
+
# SNAPSHOT_GITHUB_CLIENT_SECRET=change-me
|
|
26
|
+
# SNAPSHOT_SESSION_SECRET=change-me-generate-with-openssl-rand-base64-48
|
|
27
|
+
# SNAPSHOT_GITHUB_OWNER_LOGIN=your-github-login
|
|
28
|
+
# SNAPSHOT_GITHUB_OWNER_ID=
|
|
29
|
+
# SNAPSHOT_AUTH_ALLOWED_ORIGINS=https://ffffhx.github.io
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Codex Snapshots Share API
|
|
3
|
+
Documentation=https://github.com/ffffhx/codex-snapshots
|
|
4
|
+
After=network-online.target
|
|
5
|
+
Wants=network-online.target
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=simple
|
|
9
|
+
User=codexsnap
|
|
10
|
+
Group=codexsnap
|
|
11
|
+
WorkingDirectory=/opt/codex-snapshots
|
|
12
|
+
Environment=NODE_ENV=production
|
|
13
|
+
Environment=HOST=127.0.0.1
|
|
14
|
+
Environment=PORT=8787
|
|
15
|
+
EnvironmentFile=/etc/codex-snapshots/share-api.env
|
|
16
|
+
ExecStart=/usr/bin/env node /opt/codex-snapshots/server/share-api.mjs
|
|
17
|
+
Restart=always
|
|
18
|
+
RestartSec=3
|
|
19
|
+
NoNewPrivileges=true
|
|
20
|
+
PrivateTmp=true
|
|
21
|
+
ProtectHome=true
|
|
22
|
+
ProtectSystem=strict
|
|
23
|
+
ReadWritePaths=/var/lib/codex-snapshots
|
|
24
|
+
|
|
25
|
+
[Install]
|
|
26
|
+
WantedBy=multi-user.target
|