@lifeaitools/clauth 0.2.0 → 0.2.2
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/.clauth-skill/SKILL.md +109 -66
- package/cli/commands/install.js +24 -9
- package/cli/commands/scrub.js +231 -0
- package/cli/commands/uninstall.js +164 -0
- package/cli/index.js +54 -6
- package/package.json +1 -1
- package/supabase/migrations/001_clauth_schema.sql +12 -3
package/.clauth-skill/SKILL.md
CHANGED
|
@@ -9,93 +9,127 @@ Hardware-bound credential vault for the LIFEAI stack. Your machine is the second
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## How Claude Interfaces with clauth
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
> **CRITICAL:** Never pipe input to clauth or use interactive prompts. The CLI uses `inquirer` which produces ANSI garbage when stdin is piped. Always use one of the two methods below.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
### Method 1 — CLI with `--pw` flag (simplest)
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
All clauth commands accept `-p` / `--pw <password>` to skip the interactive password prompt:
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
```bash
|
|
21
|
+
clauth status -p "YourPassword"
|
|
22
|
+
clauth get github -p "YourPassword"
|
|
23
|
+
clauth test -p "YourPassword"
|
|
24
|
+
clauth enable github -p "YourPassword"
|
|
25
|
+
clauth write key github -p "YourPassword" # still prompts for the key value
|
|
26
|
+
```
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
### Method 2 — Direct API calls (full control, no CLI needed)
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
Call the auth-vault Edge Function directly. This is the most reliable method for Claude.
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
**Base URL:** `https://<project-ref>.supabase.co/functions/v1/auth-vault`
|
|
33
|
+
**Auth header:** `Authorization: Bearer <supabase-anon-key>`
|
|
34
|
+
**Method:** POST (all routes)
|
|
35
|
+
**Content-Type:** `application/json`
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
#### HMAC Token Derivation (must match server)
|
|
29
38
|
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
```
|
|
39
|
+
```js
|
|
40
|
+
import { createHmac, createHash, execSync } from "crypto";
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
// 1. Get machine hash (same as fingerprint.js)
|
|
43
|
+
// Windows:
|
|
44
|
+
const uuid = execSync("wmic csproduct get uuid /format:value", { encoding: "utf8" })
|
|
45
|
+
.match(/UUID=([A-F0-9-]+)/i)?.[1]?.trim();
|
|
46
|
+
const machineGuid = execSync(
|
|
47
|
+
"reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid",
|
|
48
|
+
{ encoding: "utf8" }
|
|
49
|
+
).match(/MachineGuid\s+REG_SZ\s+([a-f0-9-]+)/i)?.[1]?.trim();
|
|
50
|
+
const machineHash = createHash("sha256").update(`${uuid}:${machineGuid}`).digest("hex");
|
|
51
|
+
|
|
52
|
+
// 2. Derive HMAC token
|
|
53
|
+
const windowMs = 5 * 60 * 1000;
|
|
54
|
+
const window = Math.floor(Date.now() / windowMs);
|
|
55
|
+
const message = `${machineHash}:${window}`;
|
|
56
|
+
const token = createHmac("sha256", password).update(message).digest("hex");
|
|
57
|
+
const timestamp = window * windowMs;
|
|
37
58
|
```
|
|
38
59
|
|
|
60
|
+
#### API Routes
|
|
61
|
+
|
|
62
|
+
| Route | Body fields | Returns |
|
|
63
|
+
|-------|-------------|---------|
|
|
64
|
+
| `POST /status` | `machine_hash, token, timestamp, password` | `{ services: [...] }` |
|
|
65
|
+
| `POST /test` | `machine_hash, token, timestamp, password` | `{ ok: true }` |
|
|
66
|
+
| `POST /retrieve` | `+ service` | `{ value: "..." }` |
|
|
67
|
+
| `POST /write` | `+ service, value` | `{ ok: true }` |
|
|
68
|
+
| `POST /enable` | `+ service, enabled: bool` | `{ ok: true }` |
|
|
69
|
+
| `POST /add` | `+ name, label, key_type, description` | `{ ok: true }` |
|
|
70
|
+
| `POST /remove` | `+ service, confirm: true` | `{ ok: true }` |
|
|
71
|
+
| `POST /revoke` | `+ service, confirm: true` | `{ ok: true }` |
|
|
72
|
+
| `POST /register-machine` | `machine_hash, hmac_seed_hash, label, admin_token` | `{ ok: true }` |
|
|
73
|
+
|
|
74
|
+
All auth routes require: `machine_hash`, `token`, `timestamp`, `password`.
|
|
75
|
+
|
|
76
|
+
### Password Handling
|
|
77
|
+
|
|
78
|
+
- Ask the user for their clauth password **once per session**
|
|
79
|
+
- Store it in working memory for the duration of the conversation
|
|
80
|
+
- Never log or echo the password
|
|
81
|
+
- If the user says "use clauth" or "get my github key", ask for the password if you don't have it yet
|
|
82
|
+
|
|
39
83
|
---
|
|
40
84
|
|
|
41
|
-
|
|
85
|
+
## When someone says "install clauth"
|
|
42
86
|
|
|
43
|
-
|
|
44
|
-
```powershell
|
|
45
|
-
& "$env:USERPROFILE\.clauth\scripts\bin\bootstrap-win.exe"
|
|
46
|
-
```
|
|
87
|
+
### Step 1 — Install from npm
|
|
47
88
|
|
|
48
|
-
**Mac/Linux:**
|
|
49
89
|
```bash
|
|
50
|
-
|
|
51
|
-
# Mac:
|
|
52
|
-
~/.clauth/scripts/bin/bootstrap-macos
|
|
90
|
+
npm install -g @lifeaitools/clauth
|
|
53
91
|
```
|
|
54
92
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
93
|
+
If already installed, update:
|
|
94
|
+
```bash
|
|
95
|
+
npm update -g @lifeaitools/clauth
|
|
96
|
+
```
|
|
59
97
|
|
|
60
|
-
|
|
98
|
+
### Step 2 — Run the installer
|
|
61
99
|
|
|
62
|
-
|
|
100
|
+
Use CLI flags to avoid interactive prompts:
|
|
63
101
|
|
|
64
|
-
|
|
102
|
+
```bash
|
|
103
|
+
clauth install --ref <supabase-project-ref> --pat <personal-access-token>
|
|
104
|
+
```
|
|
65
105
|
|
|
66
|
-
**Project ref** — last part of
|
|
106
|
+
**Project ref** — last part of the Supabase project URL:
|
|
67
107
|
`https://supabase.com/dashboard/project/` **`uvojezuorjgqzmhhgluu`**
|
|
68
108
|
|
|
69
109
|
**Personal Access Token (PAT):**
|
|
70
110
|
`https://supabase.com/dashboard/account/tokens` → Generate new token
|
|
71
111
|
*(NOT the anon key or service_role — this is your account-level token)*
|
|
72
112
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
---
|
|
113
|
+
The installer provisions everything and prints a **bootstrap token** — save it.
|
|
76
114
|
|
|
77
|
-
### Step
|
|
115
|
+
### Step 3 — Setup this machine
|
|
78
116
|
|
|
79
|
-
```
|
|
80
|
-
clauth setup
|
|
117
|
+
```bash
|
|
118
|
+
clauth setup --admin-token <bootstrap-token> -p <password>
|
|
81
119
|
```
|
|
82
120
|
|
|
83
|
-
Asks: machine label, password, bootstrap token (from step 4).
|
|
84
|
-
|
|
85
121
|
Then verify:
|
|
122
|
+
```bash
|
|
123
|
+
clauth test -p <password>
|
|
124
|
+
clauth status -p <password>
|
|
86
125
|
```
|
|
87
|
-
clauth test → PASS
|
|
88
|
-
clauth status → 12 services ready
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
---
|
|
92
126
|
|
|
93
|
-
### Step
|
|
127
|
+
### Step 4 — Write your first key
|
|
94
128
|
|
|
95
129
|
```bash
|
|
96
|
-
clauth write key github # prompts for value
|
|
97
|
-
clauth enable github
|
|
98
|
-
clauth get github
|
|
130
|
+
clauth write key github -p <password> # prompts for value
|
|
131
|
+
clauth enable github -p <password>
|
|
132
|
+
clauth get github -p <password>
|
|
99
133
|
```
|
|
100
134
|
|
|
101
135
|
See `references/keys-guide.md` for where to find every credential.
|
|
@@ -105,21 +139,29 @@ See `references/keys-guide.md` for where to find every credential.
|
|
|
105
139
|
## Command reference
|
|
106
140
|
|
|
107
141
|
```
|
|
108
|
-
clauth install
|
|
109
|
-
clauth setup
|
|
110
|
-
clauth status
|
|
111
|
-
clauth test
|
|
112
|
-
clauth list
|
|
113
|
-
|
|
114
|
-
clauth write key <service>
|
|
115
|
-
clauth write pw
|
|
116
|
-
clauth enable <svc|all>
|
|
117
|
-
clauth disable <svc|all>
|
|
118
|
-
clauth get <service>
|
|
119
|
-
|
|
120
|
-
clauth add service <n>
|
|
121
|
-
clauth remove service <n>
|
|
122
|
-
clauth revoke <svc|all>
|
|
142
|
+
clauth install [--ref R] [--pat P] First-time: provision Supabase + install skill
|
|
143
|
+
clauth setup [--admin-token T] [-p P] Register this machine
|
|
144
|
+
clauth status [-p P] All services + state
|
|
145
|
+
clauth test [-p P] Verify HMAC connection
|
|
146
|
+
clauth list [-p P] Service names
|
|
147
|
+
|
|
148
|
+
clauth write key <service> [-p P] Store a credential
|
|
149
|
+
clauth write pw [-p P] Change password
|
|
150
|
+
clauth enable <svc|all> [-p P] Activate service
|
|
151
|
+
clauth disable <svc|all> [-p P] Suspend service
|
|
152
|
+
clauth get <service> [-p P] Retrieve a key
|
|
153
|
+
|
|
154
|
+
clauth add service <n> [-p P] Register new service
|
|
155
|
+
clauth remove service <n> [-p P] Remove service
|
|
156
|
+
clauth revoke <svc|all> [-p P] Delete key (destructive)
|
|
157
|
+
|
|
158
|
+
clauth scrub Scrub active transcript (most recent .jsonl)
|
|
159
|
+
clauth scrub <file> Scrub a specific file
|
|
160
|
+
clauth scrub all Scrub all transcripts in ~/.claude/projects/
|
|
161
|
+
clauth scrub all --force Rescrub everything (ignore markers)
|
|
162
|
+
|
|
163
|
+
clauth uninstall --ref R --pat P Full teardown (DB, Edge Fn, secrets, skill, config)
|
|
164
|
+
clauth uninstall --ref R --pat P --yes Skip confirmation
|
|
123
165
|
```
|
|
124
166
|
|
|
125
167
|
## Services
|
|
@@ -137,5 +179,6 @@ clauth revoke <svc|all> Delete key (destructive)
|
|
|
137
179
|
| `machine_not_found` | Run `clauth setup` |
|
|
138
180
|
| `timestamp_expired` | Sync system clock |
|
|
139
181
|
| `invalid_token` | Wrong password |
|
|
140
|
-
| `service_disabled` | `clauth enable <service>` |
|
|
141
|
-
| `no_key_stored` | `clauth write key <service>` |
|
|
182
|
+
| `service_disabled` | `clauth enable <service> -p <password>` |
|
|
183
|
+
| `no_key_stored` | `clauth write key <service> -p <password>` |
|
|
184
|
+
| ANSI garbage output | You piped stdin — use `-p` flag instead |
|
package/cli/commands/install.js
CHANGED
|
@@ -157,16 +157,31 @@ export async function runInstall(opts = {}) {
|
|
|
157
157
|
const s5 = ora('Deploying auth-vault Edge Function...').start();
|
|
158
158
|
const fnSource = readFileSync(join(ROOT, 'supabase/functions/auth-vault/index.ts'), 'utf8');
|
|
159
159
|
try {
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
160
|
+
// Use the /deploy endpoint with multipart/form-data (not the old /functions endpoint)
|
|
161
|
+
const formData = new FormData();
|
|
162
|
+
|
|
163
|
+
const metadata = {
|
|
164
|
+
name: 'auth-vault',
|
|
165
|
+
entrypoint_path: 'index.ts',
|
|
166
|
+
verify_jwt: true,
|
|
167
|
+
};
|
|
168
|
+
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
|
169
|
+
formData.append('file', new Blob([fnSource], { type: 'application/typescript' }), 'index.ts');
|
|
170
|
+
|
|
171
|
+
const deployRes = await fetch(
|
|
172
|
+
`${MGMT}/projects/${ref}/functions/deploy?slug=auth-vault`,
|
|
173
|
+
{
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: { 'Authorization': `Bearer ${pat}` },
|
|
176
|
+
body: formData,
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (!deployRes.ok) {
|
|
181
|
+
const errText = await deployRes.text().catch(() => deployRes.statusText);
|
|
182
|
+
throw new Error(`HTTP ${deployRes.status}: ${errText}`);
|
|
169
183
|
}
|
|
184
|
+
|
|
170
185
|
s5.succeed('auth-vault Edge Function deployed');
|
|
171
186
|
} catch (e) {
|
|
172
187
|
s5.warn(`Edge Function deploy failed: ${e.message}`);
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// cli/commands/scrub.js — Scrub credentials from Claude Code transcript logs
|
|
2
|
+
//
|
|
3
|
+
// clauth scrub → scrub active transcript (most recent .jsonl)
|
|
4
|
+
// clauth scrub <file> → scrub a specific file
|
|
5
|
+
// clauth scrub all → scrub every transcript .jsonl
|
|
6
|
+
// clauth scrub --force → rescrub even if already marked
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import ora from "ora";
|
|
13
|
+
|
|
14
|
+
const SCRUB_MARKER = "[CLAUTH-SCRUBBED]";
|
|
15
|
+
const SCRUB_VERSION = "1.1";
|
|
16
|
+
|
|
17
|
+
// ──────────────────────────────────────────────
|
|
18
|
+
// Credential patterns — regex + replacement
|
|
19
|
+
// ──────────────────────────────────────────────
|
|
20
|
+
const PATTERNS = [
|
|
21
|
+
// Supabase JWTs (anon, service_role)
|
|
22
|
+
[/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
|
|
23
|
+
"[SUPABASE_JWT_REDACTED]"],
|
|
24
|
+
|
|
25
|
+
// Vercel tokens
|
|
26
|
+
[/vcp_[A-Za-z0-9]{20,80}/g,
|
|
27
|
+
"[VERCEL_TOKEN_REDACTED]"],
|
|
28
|
+
|
|
29
|
+
// R2 / S3 secret access keys (64-char hex)
|
|
30
|
+
[/"secret_access_key"\s*:\s*"[a-f0-9]{64}"/g,
|
|
31
|
+
'"secret_access_key": "[R2_SECRET_REDACTED]"'],
|
|
32
|
+
|
|
33
|
+
// R2 / S3 access key IDs (32-char hex)
|
|
34
|
+
[/"access_key_id"\s*:\s*"[a-f0-9]{32}"/g,
|
|
35
|
+
'"access_key_id": "[R2_KEY_REDACTED]"'],
|
|
36
|
+
|
|
37
|
+
// Cloudflare admin tokens
|
|
38
|
+
[/"admin_token"\s*:\s*"[A-Za-z0-9_-]{20,60}"/g,
|
|
39
|
+
'"admin_token": "[CF_TOKEN_REDACTED]"'],
|
|
40
|
+
|
|
41
|
+
// Cloudflare account IDs (32-char hex)
|
|
42
|
+
[/"account_id"\s*:\s*"[a-f0-9]{32}"/g,
|
|
43
|
+
'"account_id": "[CF_ACCOUNT_REDACTED]"'],
|
|
44
|
+
|
|
45
|
+
// GitHub tokens
|
|
46
|
+
[/ghp_[A-Za-z0-9]{36}/g,
|
|
47
|
+
"[GITHUB_TOKEN_REDACTED]"],
|
|
48
|
+
[/github_pat_[A-Za-z0-9_]{40,100}/g,
|
|
49
|
+
"[GITHUB_PAT_REDACTED]"],
|
|
50
|
+
|
|
51
|
+
// Neo4j connection strings with passwords
|
|
52
|
+
[/neo4j\+s?:\/\/[^"\\]+/g,
|
|
53
|
+
"[NEO4J_CONNSTRING_REDACTED]"],
|
|
54
|
+
|
|
55
|
+
// Generic Bearer tokens in Authorization headers
|
|
56
|
+
[/Bearer [A-Za-z0-9_-]{20,}/g,
|
|
57
|
+
"Bearer [TOKEN_REDACTED]"],
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// ──────────────────────────────────────────────
|
|
61
|
+
// Marker check — read last 512 bytes
|
|
62
|
+
// ──────────────────────────────────────────────
|
|
63
|
+
function isAlreadyScrubbed(filePath) {
|
|
64
|
+
try {
|
|
65
|
+
const stat = fs.statSync(filePath);
|
|
66
|
+
const fd = fs.openSync(filePath, "r");
|
|
67
|
+
const bufSize = Math.min(512, stat.size);
|
|
68
|
+
const buf = Buffer.alloc(bufSize);
|
|
69
|
+
fs.readSync(fd, buf, 0, bufSize, Math.max(0, stat.size - bufSize));
|
|
70
|
+
fs.closeSync(fd);
|
|
71
|
+
|
|
72
|
+
const tail = buf.toString("utf-8");
|
|
73
|
+
const lastLine = tail.trim().split("\n").pop();
|
|
74
|
+
if (lastLine && lastLine.includes(SCRUB_MARKER)) {
|
|
75
|
+
try {
|
|
76
|
+
const marker = JSON.parse(lastLine);
|
|
77
|
+
return marker.version === SCRUB_VERSION;
|
|
78
|
+
} catch { return false; }
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
} catch { return false; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ──────────────────────────────────────────────
|
|
85
|
+
// Stamp file as scrubbed
|
|
86
|
+
// ──────────────────────────────────────────────
|
|
87
|
+
function stampScrubbed(filePath) {
|
|
88
|
+
const marker = JSON.stringify({
|
|
89
|
+
type: SCRUB_MARKER,
|
|
90
|
+
version: SCRUB_VERSION,
|
|
91
|
+
scrubbed_at: new Date().toISOString(),
|
|
92
|
+
patterns: PATTERNS.length,
|
|
93
|
+
});
|
|
94
|
+
fs.appendFileSync(filePath, "\n" + marker + "\n", "utf-8");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ──────────────────────────────────────────────
|
|
98
|
+
// Scrub a single file
|
|
99
|
+
// ──────────────────────────────────────────────
|
|
100
|
+
function scrubFile(filePath, force = false) {
|
|
101
|
+
if (!force && isAlreadyScrubbed(filePath)) {
|
|
102
|
+
return "skipped";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
106
|
+
let total = 0;
|
|
107
|
+
|
|
108
|
+
for (const [pattern, replacement] of PATTERNS) {
|
|
109
|
+
// Reset lastIndex for global regexes
|
|
110
|
+
pattern.lastIndex = 0;
|
|
111
|
+
const matches = content.match(pattern);
|
|
112
|
+
if (matches) {
|
|
113
|
+
total += matches.length;
|
|
114
|
+
content = content.replace(pattern, replacement);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (total > 0) {
|
|
119
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Stamp as clean (whether creds found or not)
|
|
123
|
+
stampScrubbed(filePath);
|
|
124
|
+
return total;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ──────────────────────────────────────────────
|
|
128
|
+
// Find all transcript .jsonl files
|
|
129
|
+
// ──────────────────────────────────────────────
|
|
130
|
+
function findTranscripts() {
|
|
131
|
+
const claudeDir = path.join(os.homedir(), ".claude", "projects");
|
|
132
|
+
if (!fs.existsSync(claudeDir)) return [];
|
|
133
|
+
|
|
134
|
+
const results = [];
|
|
135
|
+
function walk(dir) {
|
|
136
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
137
|
+
const full = path.join(dir, entry.name);
|
|
138
|
+
if (entry.isDirectory()) walk(full);
|
|
139
|
+
else if (entry.name.endsWith(".jsonl")) results.push(full);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
walk(claudeDir);
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ──────────────────────────────────────────────
|
|
147
|
+
// Find the most recent transcript (active session)
|
|
148
|
+
// ──────────────────────────────────────────────
|
|
149
|
+
function findMostRecent() {
|
|
150
|
+
const files = findTranscripts();
|
|
151
|
+
if (files.length === 0) return null;
|
|
152
|
+
|
|
153
|
+
let newest = files[0];
|
|
154
|
+
let newestMtime = fs.statSync(files[0]).mtimeMs;
|
|
155
|
+
for (const f of files.slice(1)) {
|
|
156
|
+
const mt = fs.statSync(f).mtimeMs;
|
|
157
|
+
if (mt > newestMtime) { newest = f; newestMtime = mt; }
|
|
158
|
+
}
|
|
159
|
+
return newest;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ──────────────────────────────────────────────
|
|
163
|
+
// Exported runner
|
|
164
|
+
// ──────────────────────────────────────────────
|
|
165
|
+
export async function runScrub(target, opts = {}) {
|
|
166
|
+
const force = opts.force || false;
|
|
167
|
+
|
|
168
|
+
// Determine which files to scrub
|
|
169
|
+
let files;
|
|
170
|
+
if (target === "all") {
|
|
171
|
+
files = findTranscripts();
|
|
172
|
+
if (files.length === 0) {
|
|
173
|
+
console.log(chalk.yellow("\n No transcript files found.\n"));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
console.log(chalk.cyan(`\n Scrubbing ${files.length} transcript file(s)...\n`));
|
|
177
|
+
} else if (target && target !== "all") {
|
|
178
|
+
// Specific file
|
|
179
|
+
const resolved = path.resolve(target);
|
|
180
|
+
if (!fs.existsSync(resolved)) {
|
|
181
|
+
console.log(chalk.red(`\n File not found: ${resolved}\n`));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
files = [resolved];
|
|
185
|
+
} else {
|
|
186
|
+
// No target — find most recent
|
|
187
|
+
const recent = findMostRecent();
|
|
188
|
+
if (!recent) {
|
|
189
|
+
console.log(chalk.yellow("\n No transcript files found.\n"));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
files = [recent];
|
|
193
|
+
console.log(chalk.gray(`\n Active transcript: ${path.basename(recent)}`));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Scrub
|
|
197
|
+
const spinner = ora("Scrubbing credentials...").start();
|
|
198
|
+
let grandTotal = 0;
|
|
199
|
+
let skipped = 0;
|
|
200
|
+
let scanned = 0;
|
|
201
|
+
|
|
202
|
+
for (const f of files) {
|
|
203
|
+
const result = scrubFile(f, force);
|
|
204
|
+
if (result === "skipped") {
|
|
205
|
+
skipped++;
|
|
206
|
+
} else {
|
|
207
|
+
scanned++;
|
|
208
|
+
if (result > 0) {
|
|
209
|
+
spinner.stop();
|
|
210
|
+
console.log(chalk.yellow(` ${result} redaction(s) in ${path.basename(f)}`));
|
|
211
|
+
spinner.start("Scrubbing credentials...");
|
|
212
|
+
grandTotal += result;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
spinner.stop();
|
|
218
|
+
|
|
219
|
+
// Summary
|
|
220
|
+
const parts = [];
|
|
221
|
+
if (scanned) parts.push(`scanned ${scanned}`);
|
|
222
|
+
if (skipped) parts.push(chalk.gray(`skipped ${skipped} (already clean)`));
|
|
223
|
+
if (grandTotal) parts.push(chalk.green(`${grandTotal} credential(s) scrubbed`));
|
|
224
|
+
else if (scanned) parts.push(chalk.green("no credentials found"));
|
|
225
|
+
|
|
226
|
+
console.log(`\n ${files.length} file(s): ${parts.join(", ")}.`);
|
|
227
|
+
if (skipped && !force) {
|
|
228
|
+
console.log(chalk.gray(" (use --force to rescan all files)"));
|
|
229
|
+
}
|
|
230
|
+
console.log();
|
|
231
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// cli/commands/uninstall.js
|
|
2
|
+
// clauth uninstall — full teardown: DB objects, Edge Function, secrets, skill, local config
|
|
3
|
+
//
|
|
4
|
+
// Reverses everything `clauth install` does:
|
|
5
|
+
// 1. Drops clauth tables, policies, triggers, functions from Supabase
|
|
6
|
+
// 2. Deletes auth-vault Edge Function
|
|
7
|
+
// 3. Removes CLAUTH_* secrets
|
|
8
|
+
// 4. Removes Claude skill directory
|
|
9
|
+
// 5. Clears local config (Conf store)
|
|
10
|
+
|
|
11
|
+
import { existsSync, rmSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import Conf from 'conf';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import ora from 'ora';
|
|
16
|
+
|
|
17
|
+
const MGMT = 'https://api.supabase.com/v1';
|
|
18
|
+
const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
|
|
19
|
+
(process.platform === 'win32'
|
|
20
|
+
? join(process.env.USERPROFILE || '', '.claude', 'skills')
|
|
21
|
+
: join(process.env.HOME || '', '.claude', 'skills'));
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────
|
|
24
|
+
// Supabase Management API helper
|
|
25
|
+
// ─────────────────────────────────────────────
|
|
26
|
+
async function mgmt(pat, method, path, body) {
|
|
27
|
+
const res = await fetch(`${MGMT}${path}`, {
|
|
28
|
+
method,
|
|
29
|
+
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
30
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const text = await res.text().catch(() => res.statusText);
|
|
34
|
+
throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
|
|
35
|
+
}
|
|
36
|
+
if (res.status === 204) return {};
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
if (!text) return {};
|
|
39
|
+
return JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────
|
|
43
|
+
// Main uninstall command
|
|
44
|
+
// ─────────────────────────────────────────────
|
|
45
|
+
export async function runUninstall(opts = {}) {
|
|
46
|
+
console.log(chalk.red('\n🗑️ clauth uninstall\n'));
|
|
47
|
+
|
|
48
|
+
const config = new Conf({ projectName: 'clauth' });
|
|
49
|
+
|
|
50
|
+
// ── Collect credentials ────────────────────
|
|
51
|
+
const ref = opts.ref || config.get('supabase_url')?.match(/https:\/\/(.+)\.supabase\.co/)?.[1];
|
|
52
|
+
const pat = opts.pat;
|
|
53
|
+
|
|
54
|
+
if (!ref) {
|
|
55
|
+
console.log(chalk.red(' Cannot determine Supabase project ref.'));
|
|
56
|
+
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (!pat) {
|
|
60
|
+
console.log(chalk.red(' Supabase PAT required for teardown.'));
|
|
61
|
+
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.gray(` Project: ${ref}\n`));
|
|
66
|
+
|
|
67
|
+
// ── Step 1: Drop database objects ──────────
|
|
68
|
+
const s1 = ora('Dropping clauth database objects...').start();
|
|
69
|
+
const teardownSQL = `
|
|
70
|
+
-- Drop triggers
|
|
71
|
+
DROP TRIGGER IF EXISTS clauth_services_updated ON public.clauth_services;
|
|
72
|
+
|
|
73
|
+
-- Drop tables (CASCADE drops policies automatically)
|
|
74
|
+
DROP TABLE IF EXISTS public.clauth_audit CASCADE;
|
|
75
|
+
DROP TABLE IF EXISTS public.clauth_machines CASCADE;
|
|
76
|
+
DROP TABLE IF EXISTS public.clauth_services CASCADE;
|
|
77
|
+
|
|
78
|
+
-- Drop functions
|
|
79
|
+
DROP FUNCTION IF EXISTS public.clauth_touch_updated() CASCADE;
|
|
80
|
+
DROP FUNCTION IF EXISTS public.clauth_upsert_vault_secret(text, text) CASCADE;
|
|
81
|
+
DROP FUNCTION IF EXISTS public.clauth_get_vault_secret(text) CASCADE;
|
|
82
|
+
DROP FUNCTION IF EXISTS public.clauth_delete_vault_secret(text) CASCADE;
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: teardownSQL });
|
|
87
|
+
s1.succeed('Database objects dropped (tables, triggers, functions, policies)');
|
|
88
|
+
} catch (e) {
|
|
89
|
+
s1.fail(`Database teardown failed: ${e.message}`);
|
|
90
|
+
console.log(chalk.yellow(' You may need to drop objects manually via SQL editor.'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Step 2: Delete Edge Function ───────────
|
|
94
|
+
const s2 = ora('Deleting auth-vault Edge Function...').start();
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(`${MGMT}/projects/${ref}/functions/auth-vault`, {
|
|
97
|
+
method: 'DELETE',
|
|
98
|
+
headers: { 'Authorization': `Bearer ${pat}` },
|
|
99
|
+
});
|
|
100
|
+
if (res.ok || res.status === 404) {
|
|
101
|
+
s2.succeed(res.status === 404
|
|
102
|
+
? 'Edge Function not found (already deleted)'
|
|
103
|
+
: 'Edge Function deleted');
|
|
104
|
+
} else {
|
|
105
|
+
const text = await res.text().catch(() => res.statusText);
|
|
106
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
s2.fail(`Edge Function delete failed: ${e.message}`);
|
|
110
|
+
console.log(chalk.yellow(' Delete manually: Supabase Dashboard → Edge Functions → auth-vault → Delete'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Step 3: Remove secrets ─────────────────
|
|
114
|
+
const s3 = ora('Removing clauth secrets...').start();
|
|
115
|
+
try {
|
|
116
|
+
// Supabase Management API: DELETE /projects/{ref}/secrets with body listing secret names
|
|
117
|
+
const res = await fetch(`${MGMT}/projects/${ref}/secrets`, {
|
|
118
|
+
method: 'DELETE',
|
|
119
|
+
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify(['CLAUTH_HMAC_SALT', 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN']),
|
|
121
|
+
});
|
|
122
|
+
if (res.ok) {
|
|
123
|
+
s3.succeed('Secrets removed (CLAUTH_HMAC_SALT, CLAUTH_ADMIN_BOOTSTRAP_TOKEN)');
|
|
124
|
+
} else {
|
|
125
|
+
const text = await res.text().catch(() => res.statusText);
|
|
126
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
s3.warn(`Secret removal failed: ${e.message}`);
|
|
130
|
+
console.log(chalk.yellow(' Remove manually: Supabase → Settings → Edge Functions → Secrets'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Step 4: Remove Claude skill ────────────
|
|
134
|
+
const s4 = ora('Removing Claude skill...').start();
|
|
135
|
+
const skillDir = join(SKILLS_DIR, 'clauth');
|
|
136
|
+
if (existsSync(skillDir)) {
|
|
137
|
+
try {
|
|
138
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
139
|
+
s4.succeed(`Skill removed: ${skillDir}`);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
s4.warn(`Could not remove skill: ${e.message}`);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
s4.succeed('Skill directory not found (already removed)');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Step 5: Clear local config ─────────────
|
|
148
|
+
const s5 = ora('Clearing local config...').start();
|
|
149
|
+
try {
|
|
150
|
+
config.clear();
|
|
151
|
+
s5.succeed('Local config cleared');
|
|
152
|
+
} catch (e) {
|
|
153
|
+
s5.warn(`Could not clear config: ${e.message}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Done ───────────────────────────────────
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
159
|
+
console.log(chalk.yellow(' ✓ clauth fully uninstalled'));
|
|
160
|
+
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(chalk.gray(' To reinstall: npx @lifeaitools/clauth install'));
|
|
163
|
+
console.log('');
|
|
164
|
+
}
|
package/cli/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as api from "./api.js";
|
|
|
11
11
|
import os from "os";
|
|
12
12
|
|
|
13
13
|
const config = new Conf({ projectName: "clauth" });
|
|
14
|
-
const VERSION = "0.2.
|
|
14
|
+
const VERSION = "0.2.2";
|
|
15
15
|
|
|
16
16
|
// ============================================================
|
|
17
17
|
// Password prompt helper
|
|
@@ -51,6 +51,8 @@ program
|
|
|
51
51
|
// clauth install (Supabase provisioning + skill install + test)
|
|
52
52
|
// ──────────────────────────────────────────────
|
|
53
53
|
import { runInstall } from './commands/install.js';
|
|
54
|
+
import { runUninstall } from './commands/uninstall.js';
|
|
55
|
+
import { runScrub } from './commands/scrub.js';
|
|
54
56
|
|
|
55
57
|
program
|
|
56
58
|
.command('install')
|
|
@@ -61,6 +63,28 @@ program
|
|
|
61
63
|
await runInstall(opts);
|
|
62
64
|
});
|
|
63
65
|
|
|
66
|
+
program
|
|
67
|
+
.command('uninstall')
|
|
68
|
+
.description('Full teardown — drop DB objects, Edge Function, secrets, skill, config')
|
|
69
|
+
.option('--ref <ref>', 'Supabase project ref')
|
|
70
|
+
.option('--pat <pat>', 'Supabase Personal Access Token (required)')
|
|
71
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
if (!opts.yes) {
|
|
74
|
+
const inquirerMod = await import('inquirer');
|
|
75
|
+
const { confirm } = await inquirerMod.default.prompt([{
|
|
76
|
+
type: 'input',
|
|
77
|
+
name: 'confirm',
|
|
78
|
+
message: chalk.red('Type "CONFIRM UNINSTALL" to proceed:'),
|
|
79
|
+
}]);
|
|
80
|
+
if (confirm !== 'CONFIRM UNINSTALL') {
|
|
81
|
+
console.log(chalk.yellow('\n Uninstall cancelled.\n'));
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
await runUninstall(opts);
|
|
86
|
+
});
|
|
87
|
+
|
|
64
88
|
// ──────────────────────────────────────────────
|
|
65
89
|
// clauth setup
|
|
66
90
|
// ──────────────────────────────────────────────
|
|
@@ -273,11 +297,17 @@ addCmd
|
|
|
273
297
|
.option("-p, --pw <password>")
|
|
274
298
|
.action(async (name, opts) => {
|
|
275
299
|
const auth = await getAuth(opts.pw);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
{
|
|
280
|
-
|
|
300
|
+
let answers;
|
|
301
|
+
if (opts.type && opts.label) {
|
|
302
|
+
// Non-interactive — all flags provided
|
|
303
|
+
answers = { label: opts.label, key_type: opts.type, desc: opts.description || "" };
|
|
304
|
+
} else {
|
|
305
|
+
answers = await inquirer.prompt([
|
|
306
|
+
{ type: "input", name: "label", message: "Label:", default: opts.label || name },
|
|
307
|
+
{ type: "list", name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
|
|
308
|
+
{ type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
|
|
309
|
+
]);
|
|
310
|
+
}
|
|
281
311
|
const spinner = ora(`Adding service: ${name}...`).start();
|
|
282
312
|
try {
|
|
283
313
|
const result = await api.addService(
|
|
@@ -396,6 +426,24 @@ program
|
|
|
396
426
|
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
397
427
|
});
|
|
398
428
|
|
|
429
|
+
// ──────────────────────────────────────────────
|
|
430
|
+
// clauth scrub [target]
|
|
431
|
+
// ──────────────────────────────────────────────
|
|
432
|
+
program
|
|
433
|
+
.command("scrub [target]")
|
|
434
|
+
.description("Scrub credentials from Claude Code transcript logs (no auth required)")
|
|
435
|
+
.option("--force", "Rescrub files even if already marked clean")
|
|
436
|
+
.addHelpText("after", `
|
|
437
|
+
Examples:
|
|
438
|
+
clauth scrub Scrub the most recent (active) transcript
|
|
439
|
+
clauth scrub <file> Scrub a specific .jsonl file
|
|
440
|
+
clauth scrub all Scrub every transcript in ~/.claude/projects/
|
|
441
|
+
clauth scrub all --force Rescrub all files (ignore markers)
|
|
442
|
+
`)
|
|
443
|
+
.action(async (target, opts) => {
|
|
444
|
+
await runScrub(target, opts);
|
|
445
|
+
});
|
|
446
|
+
|
|
399
447
|
// ──────────────────────────────────────────────
|
|
400
448
|
// clauth --help override banner
|
|
401
449
|
// ──────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -77,9 +77,17 @@ alter table public.clauth_audit enable row level security;
|
|
|
77
77
|
|
|
78
78
|
-- service_role bypasses RLS — all access from Edge Function only
|
|
79
79
|
-- No direct anon access to any clauth table
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
create policy "
|
|
80
|
+
do $$ begin
|
|
81
|
+
if not exists (select 1 from pg_policies where policyname = 'no_anon_services' and tablename = 'clauth_services') then
|
|
82
|
+
create policy "no_anon_services" on public.clauth_services for all using (false);
|
|
83
|
+
end if;
|
|
84
|
+
if not exists (select 1 from pg_policies where policyname = 'no_anon_machines' and tablename = 'clauth_machines') then
|
|
85
|
+
create policy "no_anon_machines" on public.clauth_machines for all using (false);
|
|
86
|
+
end if;
|
|
87
|
+
if not exists (select 1 from pg_policies where policyname = 'no_anon_audit' and tablename = 'clauth_audit') then
|
|
88
|
+
create policy "no_anon_audit" on public.clauth_audit for all using (false);
|
|
89
|
+
end if;
|
|
90
|
+
end $$;
|
|
83
91
|
|
|
84
92
|
-- ============================================================
|
|
85
93
|
-- Updated_at trigger
|
|
@@ -89,6 +97,7 @@ returns trigger language plpgsql as $$
|
|
|
89
97
|
begin new.updated_at = now(); return new; end;
|
|
90
98
|
$$;
|
|
91
99
|
|
|
100
|
+
drop trigger if exists clauth_services_updated on public.clauth_services;
|
|
92
101
|
create trigger clauth_services_updated
|
|
93
102
|
before update on public.clauth_services
|
|
94
103
|
for each row execute procedure public.clauth_touch_updated();
|