oopsdb 1.0.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/LICENSE +21 -21
- package/README.md +113 -108
- package/dist/commands/activate.js +7 -14
- package/dist/commands/init.d.ts +3 -1
- package/dist/commands/init.js +26 -16
- package/dist/commands/secure.js +88 -6
- package/dist/commands/shield.d.ts +3 -0
- package/dist/commands/shield.js +113 -0
- package/dist/index.js +8 -1
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +39 -23
- package/dist/utils/license.d.ts +1 -2
- package/dist/utils/license.js +1 -7
- package/package.json +68 -58
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 OopsDB Contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OopsDB Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,108 +1,113 @@
|
|
|
1
|
-
# OopsDB
|
|
2
|
-
|
|
3
|
-
**Don't let AI nuke your database.**
|
|
4
|
-
|
|
5
|
-
Auto-backup and 1-click restore for developers using Claude Code, Cursor, Windsurf, and other AI coding agents.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## The Problem
|
|
10
|
-
|
|
11
|
-
You're vibing. Claude Code is cranking through tasks. Then it decides the fastest way to fix a migration is `DROP TABLE users`. Or it runs `DELETE FROM orders` without a `WHERE` clause. Or it helpfully "cleans up" your SQLite file.
|
|
12
|
-
|
|
13
|
-
Your data is gone. Your afternoon is gone. Your will to live is negotiable.
|
|
14
|
-
|
|
15
|
-
## The Fix
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install -g oopsdb
|
|
19
|
-
oopsdb init # connect your DB (Supabase, Postgres, MySQL, SQLite)
|
|
20
|
-
oopsdb watch # auto-backup every 5 min
|
|
21
|
-
# ... AI nukes your DB ...
|
|
22
|
-
oopsdb restore # pick a snapshot, roll back instantly
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
That's it. Three commands. Your data survives the AI apocalypse.
|
|
26
|
-
|
|
27
|
-
## What It Does
|
|
28
|
-
|
|
29
|
-
- **Auto-backups** on a timer (`oopsdb watch`) — set it and forget it
|
|
30
|
-
- **Manual snapshots** (`oopsdb snapshot`) — before risky migrations or YOLO prompts
|
|
31
|
-
- **Interactive restore** (`oopsdb restore`) — pick any snapshot, roll back in seconds
|
|
32
|
-
- **Safety snapshots** — automatically backs up your current state before restoring, so you can't oops your oops
|
|
33
|
-
- **Encrypted at rest** — AES-256-CBC encryption on every backup file
|
|
34
|
-
- **Zero cloud, zero accounts** — everything stays on your machine
|
|
35
|
-
- **Streaming backups** — near-zero memory footprint regardless of DB size
|
|
36
|
-
|
|
37
|
-
## Supported Databases
|
|
38
|
-
|
|
39
|
-
| Database | Backup Tool | Restore Tool |
|
|
40
|
-
|----------|------------|--------------|
|
|
41
|
-
| **Supabase** | `pg_dump` (with Supabase flags) | `psql` |
|
|
42
|
-
| PostgreSQL (Neon, local, other hosted) | `pg_dump` | `psql` |
|
|
43
|
-
| MySQL / MariaDB | `mysqldump` | `mysql` |
|
|
44
|
-
| SQLite | `sqlite3` | `sqlite3` |
|
|
45
|
-
|
|
46
|
-
### Supabase (first-class support)
|
|
47
|
-
|
|
48
|
-
OopsDB has dedicated Supabase support. Just paste your connection string:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
oopsdb init
|
|
52
|
-
# → Select "Supabase"
|
|
53
|
-
# → Paste your connection string from Supabase Dashboard → Settings → Database
|
|
54
|
-
# → Done. SSL and Supabase-specific pg_dump flags are handled automatically.
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Supabase-specific flags applied automatically: `--no-owner`, `--no-privileges`, `--no-subscriptions`, `sslmode=require`.
|
|
58
|
-
|
|
59
|
-
## Commands
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
oopsdb init Set up your database connection
|
|
63
|
-
oopsdb watch Auto-backup every 5 minutes
|
|
64
|
-
oopsdb watch -i 1 Auto-backup every 1 minute (paranoid mode)
|
|
65
|
-
oopsdb snapshot One-time manual backup
|
|
66
|
-
oopsdb restore Interactive restore from any snapshot
|
|
67
|
-
oopsdb status View backup history and stats
|
|
68
|
-
oopsdb
|
|
69
|
-
oopsdb
|
|
70
|
-
oopsdb
|
|
71
|
-
oopsdb
|
|
72
|
-
oopsdb clean Remove all OopsDB data from project
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## How It Works
|
|
76
|
-
|
|
77
|
-
1. `oopsdb init` walks you through connecting your database. Credentials are encrypted and saved locally in `.oopsdb/config.json`.
|
|
78
|
-
2. `oopsdb watch` runs the native dump tool (`pg_dump`, `mysqldump`, or `sqlite3 .backup`) at your chosen interval. Output is streamed through AES-256-CBC encryption directly to disk — memory usage stays flat even for large databases.
|
|
79
|
-
3. `oopsdb restore` shows your snapshots with timestamps and sizes. Pick one, confirm, and your database is rolled back. It takes a safety snapshot first, so you can always undo the undo.
|
|
80
|
-
|
|
81
|
-
## Requirements
|
|
82
|
-
|
|
83
|
-
Your system needs the native database CLI tools:
|
|
84
|
-
|
|
85
|
-
- **PostgreSQL**: `pg_dump` + `psql`
|
|
86
|
-
- **MySQL**: `mysqldump` + `mysql`
|
|
87
|
-
- **SQLite**: `sqlite3`
|
|
88
|
-
|
|
89
|
-
OopsDB checks for these on `init` and gives install instructions if they're missing.
|
|
90
|
-
|
|
91
|
-
## Security
|
|
92
|
-
|
|
93
|
-
- Credentials encrypted at rest (AES-256-CBC, machine-local key)
|
|
94
|
-
- Backup files encrypted at rest (AES-256-CBC, streaming encryption)
|
|
95
|
-
- Nothing leaves your machine — no cloud, no telemetry, no accounts
|
|
96
|
-
- Add `.oopsdb/` to `.gitignore` (already in ours)
|
|
97
|
-
|
|
98
|
-
##
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
Local backups are great until the AI decides to `rm -rf .oopsdb/`. `oopsdb secure` pushes encrypted snapshots to tamper-proof cloud storage with write-once retention policies. Even if your entire machine gets wiped, your backups survive.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
##
|
|
107
|
-
|
|
108
|
-
|
|
1
|
+
# OopsDB
|
|
2
|
+
|
|
3
|
+
**Don't let AI nuke your database.**
|
|
4
|
+
|
|
5
|
+
Auto-backup and 1-click restore for developers using Claude Code, Cursor, Windsurf, and other AI coding agents.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The Problem
|
|
10
|
+
|
|
11
|
+
You're vibing. Claude Code is cranking through tasks. Then it decides the fastest way to fix a migration is `DROP TABLE users`. Or it runs `DELETE FROM orders` without a `WHERE` clause. Or it helpfully "cleans up" your SQLite file.
|
|
12
|
+
|
|
13
|
+
Your data is gone. Your afternoon is gone. Your will to live is negotiable.
|
|
14
|
+
|
|
15
|
+
## The Fix
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g oopsdb
|
|
19
|
+
oopsdb init # connect your DB (Supabase, Postgres, MySQL, SQLite)
|
|
20
|
+
oopsdb watch # auto-backup every 5 min
|
|
21
|
+
# ... AI nukes your DB ...
|
|
22
|
+
oopsdb restore # pick a snapshot, roll back instantly
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That's it. Three commands. Your data survives the AI apocalypse.
|
|
26
|
+
|
|
27
|
+
## What It Does
|
|
28
|
+
|
|
29
|
+
- **Auto-backups** on a timer (`oopsdb watch`) — set it and forget it
|
|
30
|
+
- **Manual snapshots** (`oopsdb snapshot`) — before risky migrations or YOLO prompts
|
|
31
|
+
- **Interactive restore** (`oopsdb restore`) — pick any snapshot, roll back in seconds
|
|
32
|
+
- **Safety snapshots** — automatically backs up your current state before restoring, so you can't oops your oops
|
|
33
|
+
- **Encrypted at rest** — AES-256-CBC encryption on every backup file
|
|
34
|
+
- **Zero cloud, zero accounts** — everything stays on your machine
|
|
35
|
+
- **Streaming backups** — near-zero memory footprint regardless of DB size
|
|
36
|
+
|
|
37
|
+
## Supported Databases
|
|
38
|
+
|
|
39
|
+
| Database | Backup Tool | Restore Tool |
|
|
40
|
+
|----------|------------|--------------|
|
|
41
|
+
| **Supabase** | `pg_dump` (with Supabase flags) | `psql` |
|
|
42
|
+
| PostgreSQL (Neon, local, other hosted) | `pg_dump` | `psql` |
|
|
43
|
+
| MySQL / MariaDB | `mysqldump` | `mysql` |
|
|
44
|
+
| SQLite | `sqlite3` | `sqlite3` |
|
|
45
|
+
|
|
46
|
+
### Supabase (first-class support)
|
|
47
|
+
|
|
48
|
+
OopsDB has dedicated Supabase support. Just paste your connection string:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
oopsdb init
|
|
52
|
+
# → Select "Supabase"
|
|
53
|
+
# → Paste your connection string from Supabase Dashboard → Settings → Database
|
|
54
|
+
# → Done. SSL and Supabase-specific pg_dump flags are handled automatically.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Supabase-specific flags applied automatically: `--no-owner`, `--no-privileges`, `--no-subscriptions`, `sslmode=require`.
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
oopsdb init Set up your database connection
|
|
63
|
+
oopsdb watch Auto-backup every 5 minutes
|
|
64
|
+
oopsdb watch -i 1 Auto-backup every 1 minute (paranoid mode)
|
|
65
|
+
oopsdb snapshot One-time manual backup
|
|
66
|
+
oopsdb restore Interactive restore from any snapshot
|
|
67
|
+
oopsdb status View backup history and stats
|
|
68
|
+
oopsdb secure Immutable cloud backups (Coming Soon)
|
|
69
|
+
oopsdb activate <key> Activate a Secure license
|
|
70
|
+
oopsdb deactivate Deactivate your license on this machine
|
|
71
|
+
oopsdb license Show current license status
|
|
72
|
+
oopsdb clean Remove all OopsDB data from project
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## How It Works
|
|
76
|
+
|
|
77
|
+
1. `oopsdb init` walks you through connecting your database. Credentials are encrypted and saved locally in `.oopsdb/config.json`.
|
|
78
|
+
2. `oopsdb watch` runs the native dump tool (`pg_dump`, `mysqldump`, or `sqlite3 .backup`) at your chosen interval. Output is streamed through AES-256-CBC encryption directly to disk — memory usage stays flat even for large databases.
|
|
79
|
+
3. `oopsdb restore` shows your snapshots with timestamps and sizes. Pick one, confirm, and your database is rolled back. It takes a safety snapshot first, so you can always undo the undo.
|
|
80
|
+
|
|
81
|
+
## Requirements
|
|
82
|
+
|
|
83
|
+
Your system needs the native database CLI tools:
|
|
84
|
+
|
|
85
|
+
- **PostgreSQL**: `pg_dump` + `psql`
|
|
86
|
+
- **MySQL**: `mysqldump` + `mysql`
|
|
87
|
+
- **SQLite**: `sqlite3`
|
|
88
|
+
|
|
89
|
+
OopsDB checks for these on `init` and gives install instructions if they're missing.
|
|
90
|
+
|
|
91
|
+
## Security
|
|
92
|
+
|
|
93
|
+
- Credentials encrypted at rest (AES-256-CBC, machine-local key)
|
|
94
|
+
- Backup files encrypted at rest (AES-256-CBC, streaming encryption)
|
|
95
|
+
- Nothing leaves your machine — no cloud, no telemetry, no accounts
|
|
96
|
+
- Add `.oopsdb/` to `.gitignore` (already in ours)
|
|
97
|
+
|
|
98
|
+
## Pricing
|
|
99
|
+
|
|
100
|
+
**Free** — Everything local. All databases, unlimited snapshots, encrypted backups. No limits, no accounts, no strings attached.
|
|
101
|
+
|
|
102
|
+
**Secure** ($9/mo) — Immutable cloud backups that even a rogue AI can't delete. Local backups are great until the AI decides to `rm -rf .oopsdb/`. `oopsdb secure` pushes encrypted snapshots to tamper-proof cloud storage with write-once retention policies. Even if your entire machine gets wiped, your backups survive.
|
|
103
|
+
|
|
104
|
+
Learn more at [oopsdb.com](https://oopsdb.com).
|
|
105
|
+
|
|
106
|
+
## Support
|
|
107
|
+
|
|
108
|
+
If OopsDB saved your database (and your afternoon), consider buying me a coffee!
|
|
109
|
+
[ko-fi.com/pintayo](https://ko-fi.com/pintayo)
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
|
@@ -26,18 +26,12 @@ async function activateCommand(licenseKey) {
|
|
|
26
26
|
console.log(chalk_1.default.gray(' Email: ') + chalk_1.default.cyan(license.customerEmail));
|
|
27
27
|
}
|
|
28
28
|
console.log(chalk_1.default.gray(' Variant: ') + chalk_1.default.cyan(license.variantName || license.tier));
|
|
29
|
-
if (license.tier === '
|
|
29
|
+
if (license.tier === 'secure') {
|
|
30
30
|
console.log(chalk_1.default.gray('\n You now have access to:'));
|
|
31
|
-
console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('PostgreSQL backups'));
|
|
32
|
-
console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('MySQL / MariaDB backups'));
|
|
33
|
-
console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('Supabase backups'));
|
|
34
|
-
console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('Unlimited snapshots'));
|
|
35
|
-
}
|
|
36
|
-
else if (license.tier === 'secure') {
|
|
37
|
-
console.log(chalk_1.default.gray('\n You now have access to:'));
|
|
38
|
-
console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('Everything in Pro'));
|
|
39
31
|
console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('Immutable cloud backups'));
|
|
40
|
-
console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('
|
|
32
|
+
console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('E2E encrypted cloud storage'));
|
|
33
|
+
console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('30-day retention'));
|
|
34
|
+
console.log(chalk_1.default.cyan(' ✓ ') + chalk_1.default.white('Restorable from any machine'));
|
|
41
35
|
}
|
|
42
36
|
console.log(chalk_1.default.gray('\n Next: ') + chalk_1.default.cyan('oopsdb init') + chalk_1.default.gray(' to set up your database\n'));
|
|
43
37
|
}
|
|
@@ -59,7 +53,7 @@ async function deactivateCommand() {
|
|
|
59
53
|
try {
|
|
60
54
|
await (0, license_1.deactivateLicense)();
|
|
61
55
|
spinner.succeed('License deactivated');
|
|
62
|
-
console.log(chalk_1.default.gray('\n
|
|
56
|
+
console.log(chalk_1.default.gray('\n Secure license deactivated. Local backups still work — they\'re free forever.'));
|
|
63
57
|
console.log(chalk_1.default.gray(' You can re-activate anytime with: ') + chalk_1.default.cyan('oopsdb activate <key>\n'));
|
|
64
58
|
}
|
|
65
59
|
catch (err) {
|
|
@@ -71,9 +65,8 @@ async function licenseStatusCommand() {
|
|
|
71
65
|
const license = (0, license_1.loadLicense)();
|
|
72
66
|
console.log(chalk_1.default.bold('\n OopsDB — License Status\n'));
|
|
73
67
|
if (!license) {
|
|
74
|
-
console.log(chalk_1.default.gray(' Plan: ') + chalk_1.default.white('Free'));
|
|
75
|
-
console.log(chalk_1.default.gray('
|
|
76
|
-
console.log(chalk_1.default.gray('\n Upgrade: ') + chalk_1.default.cyan('https://oopsdb.com') + chalk_1.default.gray(' → then run ') + chalk_1.default.cyan('oopsdb activate <key>\n'));
|
|
68
|
+
console.log(chalk_1.default.gray(' Plan: ') + chalk_1.default.white('Free (all local features)'));
|
|
69
|
+
console.log(chalk_1.default.gray('\n Want cloud backups? ') + chalk_1.default.cyan('https://oopsdb.com') + chalk_1.default.gray(' → then run ') + chalk_1.default.cyan('oopsdb activate <key>\n'));
|
|
77
70
|
return;
|
|
78
71
|
}
|
|
79
72
|
console.log(chalk_1.default.gray(' Plan: ') + chalk_1.default.green(license.tier.toUpperCase()));
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -45,8 +45,7 @@ const ora_1 = __importDefault(require("ora"));
|
|
|
45
45
|
const config_1 = require("../utils/config");
|
|
46
46
|
const dumper_1 = require("../utils/dumper");
|
|
47
47
|
const preflight_1 = require("../utils/preflight");
|
|
48
|
-
|
|
49
|
-
async function initCommand() {
|
|
48
|
+
async function initCommand(options = {}) {
|
|
50
49
|
console.log(chalk_1.default.bold('\n OopsDB Setup\n'));
|
|
51
50
|
console.log(chalk_1.default.gray(' Protect your database from AI-powered disasters.\n'));
|
|
52
51
|
const existing = (0, config_1.loadConfig)();
|
|
@@ -77,19 +76,6 @@ async function initCommand() {
|
|
|
77
76
|
],
|
|
78
77
|
},
|
|
79
78
|
]);
|
|
80
|
-
// Check license for non-SQLite databases
|
|
81
|
-
if ((0, license_1.requiresLicense)(dbType)) {
|
|
82
|
-
const tier = (0, license_1.getCurrentTier)();
|
|
83
|
-
if (tier === 'free') {
|
|
84
|
-
console.log(chalk_1.default.yellow('\n ⚠ ' + dbType.charAt(0).toUpperCase() + dbType.slice(1) + ' requires a Pro or Secure license.\n'));
|
|
85
|
-
console.log(chalk_1.default.white(' Free plan supports SQLite only.'));
|
|
86
|
-
console.log(chalk_1.default.gray('\n To upgrade:'));
|
|
87
|
-
console.log(chalk_1.default.cyan(' 1. ') + chalk_1.default.white('Get a license at ') + chalk_1.default.cyan('https://oopsdb.com'));
|
|
88
|
-
console.log(chalk_1.default.cyan(' 2. ') + chalk_1.default.white('Run: ') + chalk_1.default.cyan('oopsdb activate <your-license-key>'));
|
|
89
|
-
console.log(chalk_1.default.cyan(' 3. ') + chalk_1.default.white('Run: ') + chalk_1.default.cyan('oopsdb init') + chalk_1.default.gray(' again\n'));
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
79
|
// Supabase and plain Postgres both use pg_dump/psql
|
|
94
80
|
const toolType = dbType === 'supabase' ? 'postgres' : dbType;
|
|
95
81
|
// Pre-flight: check that the required DB tools are installed
|
|
@@ -169,12 +155,36 @@ async function initCommand() {
|
|
|
169
155
|
database: answers.database,
|
|
170
156
|
};
|
|
171
157
|
}
|
|
158
|
+
// Handle Master Key & Recovery
|
|
159
|
+
const isRecovery = !!options.recovery;
|
|
160
|
+
const masterKey = options.recovery || (0, config_1.generateMasterKey)();
|
|
172
161
|
const config = {
|
|
173
162
|
db: dbConfig,
|
|
174
163
|
createdAt: new Date().toISOString(),
|
|
164
|
+
masterKey,
|
|
175
165
|
};
|
|
176
166
|
(0, config_1.saveConfig)(config);
|
|
177
|
-
console.log(chalk_1.default.green('\n Config saved to .oopsdb/config.json (encrypted)\n'));
|
|
167
|
+
console.log(chalk_1.default.green('\n Config saved to .oopsdb/config.json (encrypted local block)\n'));
|
|
168
|
+
// If this is a new setup, force them to look at the master key
|
|
169
|
+
if (!isRecovery) {
|
|
170
|
+
console.log(chalk_1.default.bgRed.white.bold('\n CRITICAL SECURITY STEP: SAVE YOUR MASTER KEY '));
|
|
171
|
+
console.log(chalk_1.default.red(' If an AI deletes your project folder or you switch devices,'));
|
|
172
|
+
console.log(chalk_1.default.red(' this is the ONLY WAY to decrypt your cloud backups.'));
|
|
173
|
+
console.log(chalk_1.default.red(' Save it in 1Password or another password manager right now:\n'));
|
|
174
|
+
console.log(chalk_1.default.yellow.bold(` ${masterKey}\n`));
|
|
175
|
+
await inquirer.prompt([
|
|
176
|
+
{
|
|
177
|
+
type: 'confirm',
|
|
178
|
+
name: 'keySaved',
|
|
179
|
+
message: 'I have securely saved my Master Key.',
|
|
180
|
+
default: false,
|
|
181
|
+
validate: (input) => input ? true : 'You must save the Master Key to continue.',
|
|
182
|
+
},
|
|
183
|
+
]);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log(chalk_1.default.blue.bold('\n Recovery Mode Active: Master Key loaded from flag.\n'));
|
|
187
|
+
}
|
|
178
188
|
const { takeSnapshot } = await inquirer.prompt([
|
|
179
189
|
{
|
|
180
190
|
type: 'confirm',
|
package/dist/commands/secure.js
CHANGED
|
@@ -1,18 +1,100 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.secureCommand = secureCommand;
|
|
7
40
|
const chalk_1 = __importDefault(require("chalk"));
|
|
41
|
+
const ora_1 = __importDefault(require("ora"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const config_1 = require("../utils/config");
|
|
45
|
+
const dumper_1 = require("../utils/dumper");
|
|
8
46
|
async function secureCommand(options) {
|
|
9
47
|
console.log(chalk_1.default.bold('\n OopsDB Secure'));
|
|
10
48
|
console.log(chalk_1.default.gray(' Immutable cloud backups that even a rogue AI can\'t delete.\n'));
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
49
|
+
const config = (0, config_1.loadConfig)();
|
|
50
|
+
if (!config) {
|
|
51
|
+
console.log(chalk_1.default.red(' No config found. Run `oopsdb init` first.\n'));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
return; // Keeps TS happy
|
|
54
|
+
}
|
|
55
|
+
// Find the latest snapshot
|
|
56
|
+
const snapshots = (0, dumper_1.listSnapshots)();
|
|
57
|
+
if (snapshots.length === 0) {
|
|
58
|
+
console.log(chalk_1.default.red(' No snapshots found. Run `oopsdb snapshot` to create one.\n'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const latestFile = snapshots[0].file;
|
|
62
|
+
const fileName = path.basename(latestFile);
|
|
63
|
+
const fileSizeInBytes = fs.statSync(latestFile).size;
|
|
64
|
+
const fileSizeMB = (fileSizeInBytes / (1024 * 1024)).toFixed(2);
|
|
65
|
+
console.log(chalk_1.default.blue(` Found latest snapshot: ${fileName} (${fileSizeMB} MB)`));
|
|
66
|
+
// Placeholder URL logic: In the future, this would fetch from the Cloudflare Pages backend
|
|
67
|
+
// e.g. const res = await fetch('https://oopsdb.com/api/upload-url', { ... });
|
|
68
|
+
const placeholderUploadUrl = 'https://example-bucket.s3.amazonaws.com/placeholder-url-for-mvp';
|
|
69
|
+
console.log(chalk_1.default.gray(` Requesting secure upload token...\n`));
|
|
70
|
+
await uploadToS3(latestFile, placeholderUploadUrl);
|
|
14
71
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
72
|
+
async function uploadToS3(filePath, uploadUrl) {
|
|
73
|
+
const size = fs.statSync(filePath).size;
|
|
74
|
+
const fileStream = fs.createReadStream(filePath);
|
|
75
|
+
const spinner = (0, ora_1.default)(`Uploading to S3 Cloud Vault...`).start();
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(uploadUrl, {
|
|
78
|
+
method: 'PUT',
|
|
79
|
+
// @ts-ignore - needed for Node native fetch with streams
|
|
80
|
+
body: fileStream,
|
|
81
|
+
duplex: 'half',
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Length': size.toString()
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
spinner.fail(`Upload intercepted for MVP. The pre-signed URL must be provided by the backend.`);
|
|
88
|
+
console.log(chalk_1.default.yellow(` Reason: ${response.status} ${response.statusText}\n`));
|
|
89
|
+
console.log(chalk_1.default.gray(` (This is expected until the Cloudflare backend endpoint is reachable)\n`));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
spinner.succeed('Snapshot safely stored in immutable S3 Cloud Vault.');
|
|
93
|
+
console.log(chalk_1.default.green('\n Done! Your database is now AI-proof.\n'));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
spinner.fail(`Upload failed: ${err.message}`);
|
|
98
|
+
console.log(chalk_1.default.gray(` (Ensure the backend feature is fully implemented to receive a valid S3 URL)\n`));
|
|
99
|
+
}
|
|
18
100
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.shieldCommand = shieldCommand;
|
|
40
|
+
const net = __importStar(require("net"));
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const ora_1 = __importDefault(require("ora"));
|
|
43
|
+
const config_1 = require("../utils/config");
|
|
44
|
+
const dumper_1 = require("../utils/dumper");
|
|
45
|
+
async function shieldCommand(options) {
|
|
46
|
+
const config = (0, config_1.loadConfig)();
|
|
47
|
+
if (!config) {
|
|
48
|
+
console.log(chalk_1.default.red('\n No config found. Run `oopsdb init` first.\n'));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const proxyPort = parseInt(options.port || '5433', 10);
|
|
53
|
+
const targetPort = config.db.port || (config.db.type === 'postgres' ? 5432 : 3306);
|
|
54
|
+
const targetHost = config.db.host || 'localhost';
|
|
55
|
+
console.log(chalk_1.default.bold('\n OopsDB Shield ' + chalk_1.default.green('ACTIVE')));
|
|
56
|
+
console.log(chalk_1.default.gray(` Listening on port ${proxyPort} -> Forwarding to ${config.db.type} on ${targetPort}\n`));
|
|
57
|
+
const DESTRUCTIVE_REGEX = /(DROP\s+TABLE|TRUNCATE\s+TABLE|DELETE\s+FROM)/i;
|
|
58
|
+
const server = net.createServer((clientSocket) => {
|
|
59
|
+
const targetSocket = net.createConnection({
|
|
60
|
+
host: targetHost,
|
|
61
|
+
port: targetPort
|
|
62
|
+
});
|
|
63
|
+
targetSocket.on('error', (err) => {
|
|
64
|
+
console.log(chalk_1.default.red(`\n Database connection error: ${err.message}`));
|
|
65
|
+
clientSocket.destroy(err);
|
|
66
|
+
});
|
|
67
|
+
clientSocket.on('error', (err) => {
|
|
68
|
+
targetSocket.destroy(err);
|
|
69
|
+
});
|
|
70
|
+
// Handle traffic from DB back to Client
|
|
71
|
+
targetSocket.pipe(clientSocket);
|
|
72
|
+
// Handle traffic from Client to DB (with interception)
|
|
73
|
+
clientSocket.on('data', async (chunk) => {
|
|
74
|
+
const dataString = chunk.toString();
|
|
75
|
+
if (DESTRUCTIVE_REGEX.test(dataString)) {
|
|
76
|
+
// Destructive command detected!
|
|
77
|
+
clientSocket.pause();
|
|
78
|
+
targetSocket.pause();
|
|
79
|
+
console.log(chalk_1.default.bgRed.white.bold('\n WARNING ') + chalk_1.default.red(' Destructive command intercepted!'));
|
|
80
|
+
console.log(chalk_1.default.gray(` Command snippet: ${dataString.substring(0, 100).replace(/\n/g, ' ')}\n`));
|
|
81
|
+
const spinner = (0, ora_1.default)('Taking safety snapshot...').start();
|
|
82
|
+
try {
|
|
83
|
+
await (0, dumper_1.createSnapshot)(config.db);
|
|
84
|
+
spinner.succeed('Safety snapshot secured.');
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
spinner.fail(`Failed to take safety snapshot: ${err.message}`);
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk_1.default.yellow(' Releasing command to database...\n'));
|
|
90
|
+
targetSocket.write(chunk);
|
|
91
|
+
targetSocket.resume();
|
|
92
|
+
clientSocket.resume();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Normal query, pass it through
|
|
96
|
+
targetSocket.write(chunk);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
targetSocket.on('close', () => {
|
|
100
|
+
clientSocket.end();
|
|
101
|
+
});
|
|
102
|
+
clientSocket.on('close', () => {
|
|
103
|
+
targetSocket.end();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
server.listen(proxyPort, () => {
|
|
107
|
+
// The server is now listening
|
|
108
|
+
});
|
|
109
|
+
server.on('error', (err) => {
|
|
110
|
+
console.log(chalk_1.default.red(`\n Proxy server error: ${err.message}\n`));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|
|
113
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const snapshot_1 = require("./commands/snapshot");
|
|
|
10
10
|
const secure_1 = require("./commands/secure");
|
|
11
11
|
const clean_1 = require("./commands/clean");
|
|
12
12
|
const activate_1 = require("./commands/activate");
|
|
13
|
+
const shield_1 = require("./commands/shield");
|
|
13
14
|
// Read version from package.json at runtime
|
|
14
15
|
const pkg = require('../package.json');
|
|
15
16
|
const program = new commander_1.Command();
|
|
@@ -20,6 +21,7 @@ program
|
|
|
20
21
|
program
|
|
21
22
|
.command('init')
|
|
22
23
|
.description('Set up database connection for backups')
|
|
24
|
+
.option('--recovery <key>', 'Recover configuration using a saved Master Key')
|
|
23
25
|
.action(init_1.initCommand);
|
|
24
26
|
program
|
|
25
27
|
.command('watch')
|
|
@@ -38,6 +40,11 @@ program
|
|
|
38
40
|
.command('status')
|
|
39
41
|
.description('Show backup status and recent snapshots')
|
|
40
42
|
.action(status_1.statusCommand);
|
|
43
|
+
program
|
|
44
|
+
.command('shield')
|
|
45
|
+
.description('Start a database proxy interceptor that prevents destructive queries')
|
|
46
|
+
.option('-p, --port <port>', 'Proxy port', '5433')
|
|
47
|
+
.action(shield_1.shieldCommand);
|
|
41
48
|
program
|
|
42
49
|
.command('secure')
|
|
43
50
|
.description('Immutable cloud backups — tamper-proof, AI-proof')
|
|
@@ -46,7 +53,7 @@ program
|
|
|
46
53
|
.action(secure_1.secureCommand);
|
|
47
54
|
program
|
|
48
55
|
.command('activate <license-key>')
|
|
49
|
-
.description('Activate a
|
|
56
|
+
.description('Activate a Secure license key')
|
|
50
57
|
.action(activate_1.activateCommand);
|
|
51
58
|
program
|
|
52
59
|
.command('deactivate')
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -14,10 +14,12 @@ export interface DbConfig {
|
|
|
14
14
|
export interface OopsConfig {
|
|
15
15
|
db: DbConfig;
|
|
16
16
|
createdAt: string;
|
|
17
|
+
masterKey: string;
|
|
17
18
|
}
|
|
18
19
|
export declare function ensureConfigDir(): void;
|
|
19
20
|
export declare function saveConfig(config: OopsConfig): void;
|
|
20
21
|
export declare function loadConfig(): OopsConfig | null;
|
|
21
22
|
export declare function getBackupsDir(): string;
|
|
22
23
|
export declare function getConfigDir(): string;
|
|
24
|
+
export declare function generateMasterKey(): string;
|
|
23
25
|
export declare function getEncryptionKey(): Buffer;
|
package/dist/utils/config.js
CHANGED
|
@@ -38,45 +38,54 @@ exports.saveConfig = saveConfig;
|
|
|
38
38
|
exports.loadConfig = loadConfig;
|
|
39
39
|
exports.getBackupsDir = getBackupsDir;
|
|
40
40
|
exports.getConfigDir = getConfigDir;
|
|
41
|
+
exports.generateMasterKey = generateMasterKey;
|
|
41
42
|
exports.getEncryptionKey = getEncryptionKey;
|
|
42
43
|
const fs = __importStar(require("fs"));
|
|
43
44
|
const path = __importStar(require("path"));
|
|
44
45
|
const crypto = __importStar(require("crypto"));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
function getConfigDirPath() {
|
|
47
|
+
return path.join(process.cwd(), '.oopsdb');
|
|
48
|
+
}
|
|
49
|
+
function getConfigFilePath() {
|
|
50
|
+
return path.join(getConfigDirPath(), 'config.json');
|
|
51
|
+
}
|
|
52
|
+
function getBackupsDirPath() {
|
|
53
|
+
return path.join(getConfigDirPath(), 'backups');
|
|
54
|
+
}
|
|
55
|
+
// We encrypt the config file itself using a static machine-local key so that
|
|
56
|
+
// rogue processes can't easily read the master key in plain text from the file system.
|
|
57
|
+
// Note: This machineKey is NOT used for the database backups, only the config.json.
|
|
58
|
+
const MACHINE_KEY = crypto
|
|
50
59
|
.createHash('sha256')
|
|
51
|
-
.update(`oopsdb-${process.env.USER || 'default'}-${require('os').hostname()}`)
|
|
60
|
+
.update(`oopsdb-config-${process.env.USER || 'default'}-${require('os').hostname()}`)
|
|
52
61
|
.digest();
|
|
53
|
-
function
|
|
62
|
+
function encryptConfig(text) {
|
|
54
63
|
const iv = crypto.randomBytes(16);
|
|
55
|
-
const cipher = crypto.createCipheriv('aes-256-cbc',
|
|
64
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', MACHINE_KEY, iv);
|
|
56
65
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
57
66
|
encrypted += cipher.final('hex');
|
|
58
67
|
return iv.toString('hex') + ':' + encrypted;
|
|
59
68
|
}
|
|
60
|
-
function
|
|
69
|
+
function decryptConfig(text) {
|
|
61
70
|
const [ivHex, encrypted] = text.split(':');
|
|
62
71
|
const iv = Buffer.from(ivHex, 'hex');
|
|
63
|
-
const decipher = crypto.createDecipheriv('aes-256-cbc',
|
|
72
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', MACHINE_KEY, iv);
|
|
64
73
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
65
74
|
decrypted += decipher.final('utf8');
|
|
66
75
|
return decrypted;
|
|
67
76
|
}
|
|
68
77
|
function ensureConfigDir() {
|
|
69
78
|
try {
|
|
70
|
-
if (!fs.existsSync(
|
|
71
|
-
fs.mkdirSync(
|
|
79
|
+
if (!fs.existsSync(getConfigDirPath())) {
|
|
80
|
+
fs.mkdirSync(getConfigDirPath(), { recursive: true });
|
|
72
81
|
}
|
|
73
|
-
if (!fs.existsSync(
|
|
74
|
-
fs.mkdirSync(
|
|
82
|
+
if (!fs.existsSync(getBackupsDirPath())) {
|
|
83
|
+
fs.mkdirSync(getBackupsDirPath(), { recursive: true });
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
catch (err) {
|
|
78
87
|
if (err.code === 'EACCES') {
|
|
79
|
-
throw new Error(`Permission denied creating ${
|
|
88
|
+
throw new Error(`Permission denied creating ${getConfigDirPath()}. Check directory permissions.`);
|
|
80
89
|
}
|
|
81
90
|
if (err.code === 'ENOSPC') {
|
|
82
91
|
throw new Error('Disk full. Free up space and try again.');
|
|
@@ -86,16 +95,16 @@ function ensureConfigDir() {
|
|
|
86
95
|
}
|
|
87
96
|
function saveConfig(config) {
|
|
88
97
|
ensureConfigDir();
|
|
89
|
-
const encrypted =
|
|
90
|
-
fs.writeFileSync(
|
|
98
|
+
const encrypted = encryptConfig(JSON.stringify(config));
|
|
99
|
+
fs.writeFileSync(getConfigFilePath(), encrypted, 'utf8');
|
|
91
100
|
}
|
|
92
101
|
function loadConfig() {
|
|
93
|
-
if (!fs.existsSync(
|
|
102
|
+
if (!fs.existsSync(getConfigFilePath())) {
|
|
94
103
|
return null;
|
|
95
104
|
}
|
|
96
105
|
try {
|
|
97
|
-
const encrypted = fs.readFileSync(
|
|
98
|
-
const decrypted =
|
|
106
|
+
const encrypted = fs.readFileSync(getConfigFilePath(), 'utf8');
|
|
107
|
+
const decrypted = decryptConfig(encrypted);
|
|
99
108
|
return JSON.parse(decrypted);
|
|
100
109
|
}
|
|
101
110
|
catch {
|
|
@@ -104,11 +113,18 @@ function loadConfig() {
|
|
|
104
113
|
}
|
|
105
114
|
function getBackupsDir() {
|
|
106
115
|
ensureConfigDir();
|
|
107
|
-
return
|
|
116
|
+
return getBackupsDirPath();
|
|
108
117
|
}
|
|
109
118
|
function getConfigDir() {
|
|
110
|
-
return
|
|
119
|
+
return getConfigDirPath();
|
|
120
|
+
}
|
|
121
|
+
function generateMasterKey() {
|
|
122
|
+
return crypto.randomBytes(32).toString('hex');
|
|
111
123
|
}
|
|
112
124
|
function getEncryptionKey() {
|
|
113
|
-
|
|
125
|
+
const config = loadConfig();
|
|
126
|
+
if (!config || !config.masterKey) {
|
|
127
|
+
throw new Error('No Master Key found. Please run `oopsdb init` to set up your keys.');
|
|
128
|
+
}
|
|
129
|
+
return Buffer.from(config.masterKey, 'hex');
|
|
114
130
|
}
|
package/dist/utils/license.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type Tier = 'free' | '
|
|
1
|
+
export type Tier = 'free' | 'secure';
|
|
2
2
|
export interface LicenseInfo {
|
|
3
3
|
licenseKey: string;
|
|
4
4
|
instanceId: string;
|
|
@@ -11,7 +11,6 @@ export interface LicenseInfo {
|
|
|
11
11
|
export declare function loadLicense(): LicenseInfo | null;
|
|
12
12
|
export declare function removeLicense(): void;
|
|
13
13
|
export declare function getCurrentTier(): Tier;
|
|
14
|
-
export declare function requiresLicense(dbType: string): boolean;
|
|
15
14
|
/**
|
|
16
15
|
* Activate a license key with LemonSqueezy.
|
|
17
16
|
* Returns the license info on success, throws on failure.
|
package/dist/utils/license.js
CHANGED
|
@@ -36,7 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.loadLicense = loadLicense;
|
|
37
37
|
exports.removeLicense = removeLicense;
|
|
38
38
|
exports.getCurrentTier = getCurrentTier;
|
|
39
|
-
exports.requiresLicense = requiresLicense;
|
|
40
39
|
exports.activateLicense = activateLicense;
|
|
41
40
|
exports.deactivateLicense = deactivateLicense;
|
|
42
41
|
exports.validateLicense = validateLicense;
|
|
@@ -79,9 +78,6 @@ function getCurrentTier() {
|
|
|
79
78
|
return 'free';
|
|
80
79
|
return license.tier;
|
|
81
80
|
}
|
|
82
|
-
function requiresLicense(dbType) {
|
|
83
|
-
return dbType !== 'sqlite';
|
|
84
|
-
}
|
|
85
81
|
function getInstanceName() {
|
|
86
82
|
return `${os.userInfo().username}@${os.hostname()}`;
|
|
87
83
|
}
|
|
@@ -92,9 +88,7 @@ function tierFromVariant(variantName) {
|
|
|
92
88
|
const lower = variantName.toLowerCase();
|
|
93
89
|
if (lower.includes('secure'))
|
|
94
90
|
return 'secure';
|
|
95
|
-
|
|
96
|
-
return 'pro';
|
|
97
|
-
return 'pro'; // default paid tier
|
|
91
|
+
return 'secure'; // default paid tier
|
|
98
92
|
}
|
|
99
93
|
/**
|
|
100
94
|
* Make a POST request to LemonSqueezy license API.
|
package/package.json
CHANGED
|
@@ -1,58 +1,68 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "oopsdb",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Don't let AI nuke your database. Auto-backup and 1-click restore for developers using Claude Code,
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"oopsdb": "dist/index.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "tsc",
|
|
11
|
-
"dev": "ts-node src/index.ts",
|
|
12
|
-
"start": "node dist/index.js",
|
|
13
|
-
"prepublishOnly": "npm run build",
|
|
14
|
-
"test": "tsc && vitest run tests/unit.test.ts",
|
|
15
|
-
"test:e2e": "tsc && vitest run tests/e2e.test.ts",
|
|
16
|
-
"test:all": "tsc && vitest run"
|
|
17
|
-
},
|
|
18
|
-
"keywords": [
|
|
19
|
-
"database",
|
|
20
|
-
"backup",
|
|
21
|
-
"restore",
|
|
22
|
-
"rollback",
|
|
23
|
-
"claude-code",
|
|
24
|
-
"ai-safety",
|
|
25
|
-
"mysql",
|
|
26
|
-
"postgres",
|
|
27
|
-
"supabase",
|
|
28
|
-
"sqlite"
|
|
29
|
-
],
|
|
30
|
-
"author": "",
|
|
31
|
-
"license": "MIT",
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/pintayo/oopsdb.git"
|
|
35
|
-
},
|
|
36
|
-
"homepage": "https://oopsdb.com",
|
|
37
|
-
"engines": {
|
|
38
|
-
"node": ">=16.0.0"
|
|
39
|
-
},
|
|
40
|
-
"files": [
|
|
41
|
-
"dist",
|
|
42
|
-
"README.md",
|
|
43
|
-
"LICENSE"
|
|
44
|
-
],
|
|
45
|
-
"dependencies": {
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "oopsdb",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Don't let AI nuke your database. Auto-backup and 1-click restore for developers using Claude Code, Cursor, Windsurf, and other AI coding agents.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"oopsdb": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "ts-node src/index.ts",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"test": "tsc && vitest run tests/unit.test.ts",
|
|
15
|
+
"test:e2e": "tsc && vitest run tests/e2e.test.ts",
|
|
16
|
+
"test:all": "tsc && vitest run"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"database",
|
|
20
|
+
"backup",
|
|
21
|
+
"restore",
|
|
22
|
+
"rollback",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"ai-safety",
|
|
25
|
+
"mysql",
|
|
26
|
+
"postgres",
|
|
27
|
+
"supabase",
|
|
28
|
+
"sqlite"
|
|
29
|
+
],
|
|
30
|
+
"author": "",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/pintayo/oopsdb.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://oopsdb.com",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=16.0.0"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@aws-sdk/client-s3": "^3.1005.0",
|
|
47
|
+
"@aws-sdk/s3-request-presigner": "^3.1005.0",
|
|
48
|
+
"chalk": "^4.1.2",
|
|
49
|
+
"commander": "^12.1.0",
|
|
50
|
+
"inquirer": "^8.2.6",
|
|
51
|
+
"ora": "^5.4.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@cloudflare/workers-types": "^4.20260310.1",
|
|
55
|
+
"@types/inquirer": "^8.2.10",
|
|
56
|
+
"@types/node": "^22.0.0",
|
|
57
|
+
"testcontainers": "^11.12.0",
|
|
58
|
+
"typescript": "^5.7.0",
|
|
59
|
+
"vitest": "^4.0.18"
|
|
60
|
+
},
|
|
61
|
+
"directories": {
|
|
62
|
+
"test": "tests"
|
|
63
|
+
},
|
|
64
|
+
"types": "./dist/index.d.ts",
|
|
65
|
+
"bugs": {
|
|
66
|
+
"url": "https://github.com/pintayo/oopsdb/issues"
|
|
67
|
+
}
|
|
68
|
+
}
|