oopsdb 1.0.0 → 1.1.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 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 activate <key> Activate a Pro license
69
- oopsdb deactivate Deactivate your license on this machine
70
- oopsdb license Show current license status and plan
71
- oopsdb secure Immutable cloud backups (Coming Soon)
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
- ## Coming Soon: `oopsdb secure`
99
-
100
- Immutable cloud backups that even a rogue AI can't delete.
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
- Sign up for early access at [oopsdb.com/secure](https://oopsdb.com/secure).
105
-
106
- ## License
107
-
108
- MIT
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 === 'pro') {
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('Write-once retention'));
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 You\'re back on the Free plan (SQLite only).'));
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(' Access: ') + chalk_1.default.white('SQLite only'));
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()));
@@ -1 +1,3 @@
1
- export declare function initCommand(): Promise<void>;
1
+ export declare function initCommand(options?: {
2
+ recovery?: string;
3
+ }): Promise<void>;
@@ -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
- const license_1 = require("../utils/license");
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',
@@ -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
- console.log(chalk_1.default.yellow(' Coming Soon!'));
12
- console.log(chalk_1.default.white(' We are currently putting the finishing touches on our secure cloud backup infrastructure.'));
13
- console.log(chalk_1.default.gray(' Follow us on GitHub for updates: ') + chalk_1.default.cyan('https://github.com/pintayo/oopsdb\n'));
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
- // ─── Utilities ──────────────────────────────────────────────────────────────
16
- function delay(ms) {
17
- return new Promise((resolve) => setTimeout(resolve, ms));
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,3 @@
1
+ export declare function shieldCommand(options: {
2
+ port?: string;
3
+ }): Promise<void>;
@@ -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 Pro or Secure license key')
56
+ .description('Activate a Secure license key')
50
57
  .action(activate_1.activateCommand);
51
58
  program
52
59
  .command('deactivate')
@@ -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;
@@ -38,6 +38,7 @@ 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"));
@@ -45,22 +46,24 @@ const crypto = __importStar(require("crypto"));
45
46
  const CONFIG_DIR = path.join(process.cwd(), '.oopsdb');
46
47
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
47
48
  const BACKUPS_DIR = path.join(CONFIG_DIR, 'backups');
48
- // Simple encryption using a machine-local key derived from hostname + username
49
- const ENCRYPTION_KEY = crypto
49
+ // We encrypt the config file itself using a static machine-local key so that
50
+ // rogue processes can't easily read the master key in plain text from the file system.
51
+ // Note: This machineKey is NOT used for the database backups, only the config.json.
52
+ const MACHINE_KEY = crypto
50
53
  .createHash('sha256')
51
- .update(`oopsdb-${process.env.USER || 'default'}-${require('os').hostname()}`)
54
+ .update(`oopsdb-config-${process.env.USER || 'default'}-${require('os').hostname()}`)
52
55
  .digest();
53
- function encrypt(text) {
56
+ function encryptConfig(text) {
54
57
  const iv = crypto.randomBytes(16);
55
- const cipher = crypto.createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
58
+ const cipher = crypto.createCipheriv('aes-256-cbc', MACHINE_KEY, iv);
56
59
  let encrypted = cipher.update(text, 'utf8', 'hex');
57
60
  encrypted += cipher.final('hex');
58
61
  return iv.toString('hex') + ':' + encrypted;
59
62
  }
60
- function decrypt(text) {
63
+ function decryptConfig(text) {
61
64
  const [ivHex, encrypted] = text.split(':');
62
65
  const iv = Buffer.from(ivHex, 'hex');
63
- const decipher = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
66
+ const decipher = crypto.createDecipheriv('aes-256-cbc', MACHINE_KEY, iv);
64
67
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
65
68
  decrypted += decipher.final('utf8');
66
69
  return decrypted;
@@ -86,7 +89,7 @@ function ensureConfigDir() {
86
89
  }
87
90
  function saveConfig(config) {
88
91
  ensureConfigDir();
89
- const encrypted = encrypt(JSON.stringify(config));
92
+ const encrypted = encryptConfig(JSON.stringify(config));
90
93
  fs.writeFileSync(CONFIG_FILE, encrypted, 'utf8');
91
94
  }
92
95
  function loadConfig() {
@@ -95,7 +98,7 @@ function loadConfig() {
95
98
  }
96
99
  try {
97
100
  const encrypted = fs.readFileSync(CONFIG_FILE, 'utf8');
98
- const decrypted = decrypt(encrypted);
101
+ const decrypted = decryptConfig(encrypted);
99
102
  return JSON.parse(decrypted);
100
103
  }
101
104
  catch {
@@ -109,6 +112,13 @@ function getBackupsDir() {
109
112
  function getConfigDir() {
110
113
  return CONFIG_DIR;
111
114
  }
115
+ function generateMasterKey() {
116
+ return crypto.randomBytes(32).toString('hex');
117
+ }
112
118
  function getEncryptionKey() {
113
- return ENCRYPTION_KEY;
119
+ const config = loadConfig();
120
+ if (!config || !config.masterKey) {
121
+ throw new Error('No Master Key found. Please run `oopsdb init` to set up your keys.');
122
+ }
123
+ return Buffer.from(config.masterKey, 'hex');
114
124
  }
@@ -1,4 +1,4 @@
1
- export type Tier = 'free' | 'pro' | 'secure';
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.
@@ -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
- if (lower.includes('pro'))
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.0.0",
4
- "description": "Don't let AI nuke your database. Auto-backup and 1-click restore for developers using Claude Code, OpenClaw, 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
- "chalk": "^4.1.2",
47
- "commander": "^12.1.0",
48
- "inquirer": "^8.2.6",
49
- "ora": "^5.4.1"
50
- },
51
- "devDependencies": {
52
- "@types/inquirer": "^8.2.10",
53
- "@types/node": "^22.0.0",
54
- "testcontainers": "^11.12.0",
55
- "typescript": "^5.7.0",
56
- "vitest": "^4.0.18"
57
- }
58
- }
1
+ {
2
+ "name": "oopsdb",
3
+ "version": "1.1.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
+ }