@aifabrix/server-setup 1.1.0 → 1.2.0
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 +105 -42
- package/assets/builder/builder-server/nginx-builder-server.conf.template +4 -0
- package/assets/setup-dev-server-no-node.sh +8 -1
- package/dist/backup-db.spec.js +45 -0
- package/dist/backup.spec.js +17 -0
- package/dist/restore.spec.js +32 -0
- package/dist/ssh-cert.spec.js +17 -0
- package/dist/ubuntu.spec.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,68 +1,131 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AI Fabrix Builder Server Setup (af-server)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is the **one document** you need to get your builder-server **from zero to up and running** in your own Docker. How to use the server (users, secrets, onboarding, etc.) is in the **AI Fabrix Builder CLI** documentation—full open source: [github.com/esystemsdev/aifabrix-builder](https://github.com/esystemsdev/aifabrix-builder).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- **Restore** = push backup zip (builder.db + keys) back to server DATA_DIR.
|
|
5
|
+
---
|
|
7
6
|
|
|
8
|
-
##
|
|
7
|
+
## Why AI Fabrix Builder server?
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
- **Full AI Fabrix platform, one server, multi-developer:** You run the full AI Fabrix platform and a **separate builder-server** where multiple developers each get their **own dedicated, isolated environment** (users, certificates, secrets, workspaces). One installer (af-server) sets up Docker, nginx, SSL proxy, sync user, and backup on that server.
|
|
10
|
+
- **One tool for install and management:** The same **AI Fabrix Builder** CLI gets you the platform and manages users, secrets, and onboarding. All how-to in [Builder CLI docs](https://github.com/esystemsdev/aifabrix-builder).
|
|
11
|
+
- **Security and standards:** TLS, client certificates, no secrets in version control. Backups are explicit (config + DB + keys); store them encrypted.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
# Install server (Docker, nginx, SSL proxy, sync user, cron).
|
|
14
|
-
af-server install [ user@host ] [ -d DATA_DIR ] [ --dev-domain DOMAIN ] [ --ssl-dir PATH ] [ -i SSH_KEY ]
|
|
13
|
+
---
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
af-server backup [ user@host ] [ -d DATA_DIR ] [ -o output.zip ] [ -i SSH_KEY ]
|
|
15
|
+
## Getting the builder-server Docker image
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
af-server backup [ user@host ] --schedule [ --backup-dir PATH ] [ --keep-days N ] [ -i SSH_KEY ]
|
|
17
|
+
**The builder-server Docker image is not on a public registry.** You get it with the **AI Fabrix platform**.
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
1. Install the **AI Fabrix Builder** CLI: `npm install -g @aifabrix/builder`
|
|
20
|
+
2. Use the CLI to run or deploy the platform (including builder-server). See [AI Fabrix Builder](https://github.com/esystemsdev/aifabrix-builder) and [docs/README.md](https://github.com/esystemsdev/aifabrix-builder/blob/main/docs/README.md).
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
af-server ssh-cert request [ --user ID ] [ -i SSH_KEY ]
|
|
27
|
-
af-server ssh-cert install [ user@host ] [ -i SSH_KEY ]
|
|
28
|
-
```
|
|
22
|
+
Once you have the platform (and the builder-server image), use **af-server** to install that server on your own host.
|
|
29
23
|
|
|
30
|
-
|
|
24
|
+
---
|
|
31
25
|
|
|
32
26
|
## Prerequisites
|
|
33
27
|
|
|
34
|
-
- Node.js
|
|
35
|
-
- SSH access to the server (key-based auth
|
|
28
|
+
- **Node.js ≥ 18** (where you run af-server)
|
|
29
|
+
- **SSH access** to the server (key-based auth). af-server uses `~/.ssh/id_ed25519.pub` or `~/.ssh/id_rsa.pub` by default; use `-i path/to/key` if your key is elsewhere.
|
|
30
|
+
- **Server:** Ubuntu. For remote install the script can install Docker; you provide domain, SSL directory, and certificates (next section).
|
|
36
31
|
|
|
37
|
-
|
|
32
|
+
---
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
- **JSON files** — legacy; on-demand backup still works (exports to SQLite inside the zip).
|
|
34
|
+
## Manual prerequisites (before install)
|
|
41
35
|
|
|
42
|
-
|
|
36
|
+
Do the steps in [What you must do before running af-server](#what-you-must-do-before-running-af-server) (below): Ubuntu version, root/sudo, SSH, DNS (CNAME or A), SSL directory, and certificate + key. The install script does **not** create or obtain certificates; it configures nginx to use the files you provide.
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
- `backup.db` — SQLite (either copy of builder.db or export from JSON)
|
|
46
|
-
- `ca.crt`, `ca.key`, `secrets-encryption.key` — for restore
|
|
38
|
+
---
|
|
47
39
|
|
|
48
|
-
|
|
40
|
+
## Install: from zero to running
|
|
49
41
|
|
|
50
|
-
|
|
42
|
+
**From your PC** (SSH to server):
|
|
51
43
|
|
|
52
|
-
|
|
44
|
+
```bash
|
|
45
|
+
af-server install user@host
|
|
46
|
+
```
|
|
53
47
|
|
|
54
|
-
|
|
48
|
+
**On the server itself** (Ubuntu, no target):
|
|
55
49
|
|
|
56
50
|
```bash
|
|
57
|
-
|
|
58
|
-
npm install
|
|
59
|
-
npm run build
|
|
60
|
-
node dist/cli.js --help
|
|
61
|
-
# or: npm link && af-server --help
|
|
51
|
+
af-server install
|
|
62
52
|
```
|
|
63
53
|
|
|
64
|
-
|
|
54
|
+
After install, your builder-server is up. Use the **AI Fabrix Builder CLI** for all usage (users, secrets, certs, etc.)—see [Builder documentation](https://github.com/esystemsdev/aifabrix-builder/blob/main/docs/developer-isolation.md).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
|
|
60
|
+
| Command | Description |
|
|
61
|
+
| -------- | ----------- |
|
|
62
|
+
| `af-server install [ user@host ] [ -d DATA_DIR ] [ --dev-domain DOMAIN ] [ --ssl-dir PATH ] [ -i SSH_KEY ]` | Install: Docker, nginx, SSL proxy, sync user, cron. |
|
|
63
|
+
| `af-server backup [ user@host ] [ -d DATA_DIR ] [ -o output.zip ] [ -i SSH_KEY ]` | On-demand backup (config + DB + keys). |
|
|
64
|
+
| `af-server backup [ user@host ] --schedule [ --backup-dir PATH ] [ --keep-days N ] [ -i SSH_KEY ]` | Cron backup (daily 02:00, keep last N, default 7). |
|
|
65
|
+
| `af-server restore backup.zip [ user@host ] [ -d DATA_DIR ] [ --force ] [ -i SSH_KEY ]` | Restore backup to DATA_DIR. |
|
|
66
|
+
| `af-server ssh-cert install [ user@host ] [ -i SSH_KEY ]` | Add your SSH public key to server (passwordless auth). |
|
|
67
|
+
|
|
68
|
+
Backups contain secrets; store encrypted. Cron backup needs SQLite (`builder.db`) and `zip` on the server; default backup dir: `/opt/aifabrix/backups`.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Documentation
|
|
73
|
+
|
|
74
|
+
All **how to use** the builder-server (users, secrets, onboarding, developer isolation, etc.) is in the open-source **AI Fabrix Builder** docs:
|
|
75
|
+
|
|
76
|
+
- [AI Fabrix Builder](https://github.com/esystemsdev/aifabrix-builder) — repo and README
|
|
77
|
+
- [docs/README.md](https://github.com/esystemsdev/aifabrix-builder/blob/main/docs/README.md) — full documentation index
|
|
78
|
+
|
|
79
|
+
This README is only: **zero to up and running your builder server**.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## What you must do before running af-server
|
|
84
|
+
|
|
85
|
+
Complete these **manually** before `af-server install`. The install script does not create DNS, certificates, or the admin account; it expects them to exist.
|
|
86
|
+
|
|
87
|
+
**Server and access**
|
|
88
|
+
|
|
89
|
+
- **Ubuntu** — 22.04 LTS or later (recommended). Other Debian-based systems may work but are not guaranteed.
|
|
90
|
+
- **Root or sudo** — The install script must run with root (or sudo). From your PC you SSH as a user that can `sudo` to root; on the server you run `sudo af-server install` (or as root).
|
|
91
|
+
- **SSH access** — You need key-based or password SSH to the server so you can run `af-server install user@host`, or you run install locally on the server.
|
|
92
|
+
|
|
93
|
+
**DNS (CNAME or A record)**
|
|
94
|
+
|
|
95
|
+
- Create a **DNS A record** or **CNAME** so your chosen domain points to the server’s IP.
|
|
96
|
+
- Default domain used by the script: `builder01.aifabrix.dev`. To use another domain, set `--dev-domain` or env `DEV_DOMAIN`.
|
|
97
|
+
- Ensure DNS has propagated before install (nginx will use this domain).
|
|
98
|
+
|
|
99
|
+
**SSL directory and certificates**
|
|
100
|
+
|
|
101
|
+
- Create the SSL directory (default `/opt/aifabrix/ssl`):
|
|
102
|
+
`sudo mkdir -p /opt/aifabrix/ssl`
|
|
103
|
+
To use another path, set `--ssl-dir` or env `SSL_DIR`.
|
|
104
|
+
- **Obtain** a TLS certificate and private key (e.g. Let’s Encrypt, internal CA, or purchased). Full chain if applicable.
|
|
105
|
+
- **Place** in the SSL directory:
|
|
106
|
+
- `wildcard.crt` — certificate (full chain if applicable)
|
|
107
|
+
- `wildcard.key` — private key
|
|
108
|
+
Set permissions: `chmod 600 /opt/aifabrix/ssl/wildcard.key`
|
|
109
|
+
If your files have other names (e.g. `fullchain.pem` / `privkey.pem`), symlink them to `wildcard.crt` and `wildcard.key`.
|
|
110
|
+
- The install script **does not** create or obtain certificates; it only configures nginx to use these files.
|
|
111
|
+
|
|
112
|
+
**Admin user (optional but typical)**
|
|
113
|
+
If you SSH as a non-root user, that user must be able to sudo. The script will add an admin user (default `serveradmin`) to the `docker` group and grant it passwordless sudo. Ensure that user exists on the server if you rely on it, or set `SETUP_ADMIN_USER` to your SSH user.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## High level: what the install does on your Ubuntu server
|
|
118
|
+
|
|
119
|
+
When you run `af-server install`, the script (as root) does the following on the server:
|
|
120
|
+
|
|
121
|
+
- **System** — `apt update` and `apt upgrade`; optional hostname change if `SETUP_HOSTNAME` is set.
|
|
122
|
+
- **Docker** — Installs Docker if missing; enables and starts the Docker service.
|
|
123
|
+
- **Admin user** — Adds the admin user (default `serveradmin`) to the `docker` group and grants passwordless sudo (`/etc/sudoers.d/<admin>`).
|
|
124
|
+
- **Nginx** — Installs nginx if missing; enables and starts it. Generates a site config for your domain that proxies HTTPS to the builder-server container (using `wildcard.crt` and `wildcard.key` from your SSL dir).
|
|
125
|
+
- **Builder-server data dir** — Creates the data directory (default `/opt/aifabrix/builder-server/data`), sets ownership for the container. Starts the `builder-server` container if the image already exists on the server; otherwise prints how to build and run it (you get the image from the AI Fabrix platform).
|
|
126
|
+
- **Sync user** — Creates a system user (default `aifabrix-sync`) for Mutagen SSH sync; home under the data dir; creates `.ssh` and `authorized_keys`.
|
|
127
|
+
- **Cron job** — Installs a cron job (every 2 minutes) that copies the managed `authorized_keys` file into the sync user’s `.ssh/authorized_keys`.
|
|
128
|
+
- **Mutagen** — Downloads and installs the Mutagen binary; creates a systemd service and starts the daemon.
|
|
129
|
+
- **Optional** — If `INSTALL_PORTAINER=1`, installs the Portainer container. If Docker TLS is not skipped, writes `/etc/docker/daemon.json`; you can use the **same certificate** from `/opt/aifabrix/ssl` (e.g. symlink or copy `wildcard.crt` and `wildcard.key` to the paths Docker expects: `/etc/docker/server-cert.pem`, `/etc/docker/server-key.pem`, and if needed `ca.pem` for the CA), or provide separate Docker TLS certs.
|
|
65
130
|
|
|
66
|
-
|
|
67
|
-
- [builder/builder-server/README.md](../builder/builder-server/README.md) — builder-server data dir and mounts
|
|
68
|
-
- [AI Fabrix Builder CLI](https://github.com/esystemsdev/aifabrix-builder/blob/main/docs/CLI-REFERENCE.md) — for onboarding and certs (talks to builder-server API)
|
|
131
|
+
Result: your Ubuntu server has Docker, nginx (HTTPS for your domain), the builder-server container (if image present), the sync user and key sync, and Mutagen—ready for the Builder CLI to use.
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
# Generated from template by af-server install (substitutes DEV_DOMAIN, SSL_DIR, BUILDER_SERVER_PORT, DATA_DIR).
|
|
3
3
|
# SSL cert and key from SSL_DIR_PLACEHOLDER (wildcard.crt, wildcard.key).
|
|
4
4
|
# Client cert: ssl_client_certificate uses DATA_DIR_PLACEHOLDER/ca.crt (Builder CA; created by builder-server on first run).
|
|
5
|
+
# If the backend returns 400 Bad Request for requests with client cert, nginx may be sending $ssl_client_cert with
|
|
6
|
+
# literal newlines (header folding). Use njs to send cert on one line: load_module modules/ngx_http_js_module.so;
|
|
7
|
+
# then js_set $client_cert_escaped cert.oneline; js_include cert-escaped.js; and proxy_set_header X-Client-Cert $client_cert_escaped;
|
|
8
|
+
# (cert-escaped.js: function cert(r){return r.variables.ssl_client_cert?r.variables.ssl_client_cert.replace(/\n/g,'\\n'):'';}).
|
|
5
9
|
# Reload nginx after placing: sudo nginx -t && sudo systemctl reload nginx.
|
|
6
10
|
|
|
7
11
|
server {
|
|
@@ -116,6 +116,11 @@ if [ ! -f "$NGINX_CONF" ] && [ -f "$NGINX_TEMPLATE" ]; then
|
|
|
116
116
|
elif [ ! -f "$NGINX_CONF" ]; then
|
|
117
117
|
echo "Warning: SSL prereqs required. Place $NGINX_CONF (see SETUP.md) and ensure $SSL_DIR/wildcard.crt and $SSL_DIR/wildcard.key exist."
|
|
118
118
|
fi
|
|
119
|
+
if command -v nginx >/dev/null 2>&1; then
|
|
120
|
+
if nginx -t 2>/dev/null; then
|
|
121
|
+
systemctl reload nginx
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
119
124
|
|
|
120
125
|
# --- Mutagen ---
|
|
121
126
|
if ! command -v mutagen >/dev/null 2>&1; then
|
|
@@ -166,8 +171,10 @@ DOCKER_EOF
|
|
|
166
171
|
fi
|
|
167
172
|
|
|
168
173
|
# --- Builder-server data dir and container ---
|
|
174
|
+
# Container runs as uid 1001 (nodejs); data dir must be writable for bootstrap (key, CA, DB).
|
|
169
175
|
mkdir -p "$DATA_DIR"
|
|
170
|
-
chown 1001:65533 "$DATA_DIR"
|
|
176
|
+
chown -R 1001:65533 "$DATA_DIR"
|
|
177
|
+
chmod 755 "$DATA_DIR"
|
|
171
178
|
if command -v docker >/dev/null 2>&1; then
|
|
172
179
|
if ! docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^builder-server$'; then
|
|
173
180
|
echo "Builder-server container not found. Build and run manually (see builder/builder-server/README.md):"
|
package/dist/backup-db.spec.js
CHANGED
|
@@ -77,6 +77,14 @@ describe('backup-db', () => {
|
|
|
77
77
|
expect(row).toEqual({ id: '03', name: 'C' });
|
|
78
78
|
db.close();
|
|
79
79
|
});
|
|
80
|
+
it('imports no users when users.json is empty object (no .users key, not array)', () => {
|
|
81
|
+
const outPath = path.join(tmpDir, 'empty-users.db');
|
|
82
|
+
createBackupDbFromJson(outPath, { users: '{}' }, config);
|
|
83
|
+
const db = new Database(outPath, { readonly: true });
|
|
84
|
+
const count = db.prepare('SELECT COUNT(*) as n FROM users').get();
|
|
85
|
+
expect(count.n).toBe(0);
|
|
86
|
+
db.close();
|
|
87
|
+
});
|
|
80
88
|
it('imports pins from tokens.json shape', () => {
|
|
81
89
|
const outPath = path.join(tmpDir, 'out.db');
|
|
82
90
|
const tokensJson = JSON.stringify({
|
|
@@ -129,6 +137,14 @@ describe('backup-db', () => {
|
|
|
129
137
|
expect(row.user_id).toBe('03');
|
|
130
138
|
db.close();
|
|
131
139
|
});
|
|
140
|
+
it('imports no pins when tokens.json is empty object (no .pins key, not array)', () => {
|
|
141
|
+
const outPath = path.join(tmpDir, 'empty-pins.db');
|
|
142
|
+
createBackupDbFromJson(outPath, { tokens: '{}' }, config);
|
|
143
|
+
const db = new Database(outPath, { readonly: true });
|
|
144
|
+
const count = db.prepare('SELECT COUNT(*) as n FROM pin_tokens').get();
|
|
145
|
+
expect(count.n).toBe(0);
|
|
146
|
+
db.close();
|
|
147
|
+
});
|
|
132
148
|
it('imports secrets from secrets.json shape (camelCase)', () => {
|
|
133
149
|
const outPath = path.join(tmpDir, 'out.db');
|
|
134
150
|
const secretsJson = JSON.stringify({
|
|
@@ -159,6 +175,21 @@ describe('backup-db', () => {
|
|
|
159
175
|
expect(row.auth_tag).toBe('tag2');
|
|
160
176
|
db.close();
|
|
161
177
|
});
|
|
178
|
+
it('skips secret entries with null or invalid shape', () => {
|
|
179
|
+
const outPath = path.join(tmpDir, 'secrets-skip.db');
|
|
180
|
+
const secretsJson = JSON.stringify({
|
|
181
|
+
secrets: {
|
|
182
|
+
valid: { iv: 'i', auth_tag: 't', cipher: 'c' },
|
|
183
|
+
nullEntry: null,
|
|
184
|
+
invalid: { iv: 'i' },
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
createBackupDbFromJson(outPath, { secrets: secretsJson }, config);
|
|
188
|
+
const db = new Database(outPath, { readonly: true });
|
|
189
|
+
const rows = db.prepare('SELECT key FROM secrets').all();
|
|
190
|
+
expect(rows.map((r) => r.key)).toEqual(['valid']);
|
|
191
|
+
db.close();
|
|
192
|
+
});
|
|
162
193
|
it('imports ssh_keys from byUser shape', () => {
|
|
163
194
|
const outPath = path.join(tmpDir, 'out.db');
|
|
164
195
|
const sshKeysJson = JSON.stringify({
|
|
@@ -213,6 +244,20 @@ describe('backup-db', () => {
|
|
|
213
244
|
expect(count.n).toBe(0);
|
|
214
245
|
db.close();
|
|
215
246
|
});
|
|
247
|
+
it('imports ssh_keys with createdAt (camelCase) only', () => {
|
|
248
|
+
const outPath = path.join(tmpDir, 'ssh-createdAt.db');
|
|
249
|
+
const sshKeysJson = JSON.stringify({
|
|
250
|
+
byUser: {
|
|
251
|
+
'01': [{ publicKey: 'ssh-rsa AAAA', label: 'l', fingerprint: 'f', createdAt: '2025-06-01T00:00:00Z' }],
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
createBackupDbFromJson(outPath, { sshKeys: sshKeysJson }, config);
|
|
255
|
+
const db = new Database(outPath, { readonly: true });
|
|
256
|
+
const row = db.prepare('SELECT user_id, created_at FROM ssh_keys').get();
|
|
257
|
+
expect(row.user_id).toBe('01');
|
|
258
|
+
expect(row.created_at).toBe('2025-06-01T00:00:00Z');
|
|
259
|
+
db.close();
|
|
260
|
+
});
|
|
216
261
|
});
|
|
217
262
|
describe('copyBuilderDbAsBackup', () => {
|
|
218
263
|
it('copies users and pin_tokens from source db to backup', () => {
|
package/dist/backup.spec.js
CHANGED
|
@@ -183,6 +183,23 @@ describe('runBackupLocal', () => {
|
|
|
183
183
|
const config = JSON.parse(fs.readFileSync(path.join(tmpExtract, CONFIG_JSON), 'utf8'));
|
|
184
184
|
expect(config.source).toBe('json');
|
|
185
185
|
});
|
|
186
|
+
it('uses fallback JSON when a local JSON file is missing (catch path)', async () => {
|
|
187
|
+
fs.writeFileSync(path.join(dataDir, 'users.json'), '{"users":[]}');
|
|
188
|
+
fs.writeFileSync(path.join(dataDir, 'tokens.json'), '{"pins":[]}');
|
|
189
|
+
// omit secrets.json and ssh-public-keys.json so readFileSync throws and fallback is used
|
|
190
|
+
const outPath = path.join(dataDir, 'fallback.zip');
|
|
191
|
+
const result = await runBackupLocal({ dataDir, outputPath: outPath });
|
|
192
|
+
expect(fs.existsSync(result)).toBe(true);
|
|
193
|
+
const tmpExtract = path.join(dataDir, 'extract-fallback');
|
|
194
|
+
fs.mkdirSync(tmpExtract, { recursive: true });
|
|
195
|
+
await extract(result, { dir: tmpExtract });
|
|
196
|
+
const db = new Database(path.join(tmpExtract, BACKUP_DB), { readonly: true });
|
|
197
|
+
const userCount = db.prepare('SELECT COUNT(*) as n FROM users').get();
|
|
198
|
+
const secretCount = db.prepare('SELECT COUNT(*) as n FROM secrets').get();
|
|
199
|
+
expect(userCount.n).toBe(0);
|
|
200
|
+
expect(secretCount.n).toBe(0);
|
|
201
|
+
db.close();
|
|
202
|
+
});
|
|
186
203
|
it('includes key files when present in dataDir', async () => {
|
|
187
204
|
const dbPath = path.join(dataDir, BUILDER_DB);
|
|
188
205
|
const db = new Database(dbPath);
|
package/dist/restore.spec.js
CHANGED
|
@@ -212,4 +212,36 @@ describe('runRestoreLocal', () => {
|
|
|
212
212
|
fs.writeFileSync(path.join(dataDir, BUILDER_DB), 'existing');
|
|
213
213
|
await expect(runRestoreLocal({ zipPath, dataDir })).rejects.toThrow('DATA_DIR already has builder.db');
|
|
214
214
|
});
|
|
215
|
+
it('uses config.dataDir from zip when options.dataDir not provided', async () => {
|
|
216
|
+
const zipContentDir = path.join(workDir, 'content');
|
|
217
|
+
fs.mkdirSync(zipContentDir, { recursive: true });
|
|
218
|
+
const dbPath = path.join(zipContentDir, BACKUP_DB);
|
|
219
|
+
const db = new Database(dbPath);
|
|
220
|
+
db.exec('CREATE TABLE users (id TEXT PRIMARY KEY); INSERT INTO users (id) VALUES (\'1\');');
|
|
221
|
+
db.close();
|
|
222
|
+
const configFromZip = { dataDir: path.join(workDir, 'restore-from-config'), createdAt: new Date().toISOString(), source: 'builder.db' };
|
|
223
|
+
fs.writeFileSync(path.join(zipContentDir, CONFIG_JSON), JSON.stringify(configFromZip));
|
|
224
|
+
for (const k of KEY_FILES) {
|
|
225
|
+
fs.writeFileSync(path.join(zipContentDir, k), `content-${k}`);
|
|
226
|
+
}
|
|
227
|
+
await new Promise((resolve, reject) => {
|
|
228
|
+
const archive = archiver('zip', { zlib: { level: 6 } });
|
|
229
|
+
const out = fs.createWriteStream(zipPath);
|
|
230
|
+
out.on('close', () => resolve());
|
|
231
|
+
archive.on('error', reject);
|
|
232
|
+
archive.pipe(out);
|
|
233
|
+
archive.file(dbPath, { name: BACKUP_DB });
|
|
234
|
+
archive.file(path.join(zipContentDir, CONFIG_JSON), { name: CONFIG_JSON });
|
|
235
|
+
for (const k of KEY_FILES) {
|
|
236
|
+
archive.file(path.join(zipContentDir, k), { name: k });
|
|
237
|
+
}
|
|
238
|
+
archive.finalize();
|
|
239
|
+
});
|
|
240
|
+
await runRestoreLocal({ zipPath, force: true });
|
|
241
|
+
expect(fs.existsSync(path.join(configFromZip.dataDir, BUILDER_DB))).toBe(true);
|
|
242
|
+
const restoredDb = new Database(path.join(configFromZip.dataDir, BUILDER_DB), { readonly: true });
|
|
243
|
+
const user = restoredDb.prepare('SELECT id FROM users').get();
|
|
244
|
+
expect(user.id).toBe('1');
|
|
245
|
+
restoredDb.close();
|
|
246
|
+
});
|
|
215
247
|
});
|
package/dist/ssh-cert.spec.js
CHANGED
|
@@ -64,6 +64,13 @@ describe('ssh-cert', () => {
|
|
|
64
64
|
await expect(runSshCertInstall({ target: 'user@host' })).rejects.toThrow('Could not read authorized_keys');
|
|
65
65
|
expect(mockClose).toHaveBeenCalledWith(mockConn);
|
|
66
66
|
});
|
|
67
|
+
it('appends newline before new key when existing content does not end with newline', async () => {
|
|
68
|
+
mockReadFile.mockResolvedValueOnce(Buffer.from('existing key line', 'utf8'));
|
|
69
|
+
await runSshCertInstall({ target: 'user@host' });
|
|
70
|
+
const written = mockWriteFile.mock.calls[0][2].toString('utf8');
|
|
71
|
+
expect(written).toMatch(/existing key line\n.*# af-server/);
|
|
72
|
+
expect(written).toContain(KEY_LINE);
|
|
73
|
+
});
|
|
67
74
|
});
|
|
68
75
|
describe('runSshCertInstallLocal', () => {
|
|
69
76
|
let fakeHome;
|
|
@@ -97,5 +104,15 @@ describe('ssh-cert', () => {
|
|
|
97
104
|
expect(afterSecond).toBe(afterFirst);
|
|
98
105
|
expect(console.log).toHaveBeenCalledWith('Key already installed.');
|
|
99
106
|
});
|
|
107
|
+
it('appends newline before new key when authorized_keys exists and does not end with newline', () => {
|
|
108
|
+
const sshDir = path.join(fakeHome, '.ssh');
|
|
109
|
+
fs.mkdirSync(sshDir, { recursive: true });
|
|
110
|
+
const authKeysPath = path.join(sshDir, 'authorized_keys');
|
|
111
|
+
fs.writeFileSync(authKeysPath, 'existing key line', { mode: 0o600 });
|
|
112
|
+
runSshCertInstallLocal(undefined, { homedir: fakeHome });
|
|
113
|
+
const content = fs.readFileSync(authKeysPath, 'utf8');
|
|
114
|
+
expect(content).toMatch(/existing key line\n.*# af-server/);
|
|
115
|
+
expect(content).toContain(KEY_LINE);
|
|
116
|
+
});
|
|
100
117
|
});
|
|
101
118
|
});
|
package/dist/ubuntu.spec.js
CHANGED
|
@@ -53,4 +53,13 @@ describe('ubuntu', () => {
|
|
|
53
53
|
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
54
54
|
expect(() => requireUbuntu({ osReleasePath: path.join(TEST_TMP, 'nonexistent') })).toThrow(/Local mode is supported only on Ubuntu/);
|
|
55
55
|
});
|
|
56
|
+
it('uses default os-release path when options is empty object', () => {
|
|
57
|
+
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
58
|
+
try {
|
|
59
|
+
requireUbuntu({});
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
expect(e.message).toMatch(/Local mode is supported only on Ubuntu/);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
56
65
|
});
|