@lifeaitools/clauth 0.3.11 → 0.4.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/.clauth-skill/SKILL.md +184 -184
- package/.clauth-skill/references/keys-guide.md +270 -270
- package/.clauth-skill/references/operator-guide.md +148 -148
- package/README.md +125 -125
- package/cli/api.js +113 -112
- package/cli/commands/install.js +265 -264
- package/cli/commands/scrub.js +231 -231
- package/cli/commands/serve.js +514 -74
- package/cli/commands/uninstall.js +164 -164
- package/cli/conf-path.js +21 -0
- package/cli/fingerprint.js +91 -91
- package/cli/index.js +6 -2
- package/install.ps1 +44 -44
- package/install.sh +38 -38
- package/package.json +54 -54
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bootstrap.cjs +43 -43
- package/scripts/build.sh +45 -45
- package/supabase/functions/auth-vault/index.ts +235 -235
- package/supabase/migrations/001_clauth_schema.sql +103 -103
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
|
@@ -1,164 +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
|
-
}
|
|
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/conf-path.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// conf-path.js
|
|
2
|
+
// Returns Conf options that always resolve to the real system AppData on Windows.
|
|
3
|
+
//
|
|
4
|
+
// Problem: When clauth runs inside an Electron app (e.g. Claude Desktop), %APPDATA%
|
|
5
|
+
// is sandboxed to a package-specific path. The daemon runs as a plain system process
|
|
6
|
+
// using the real %APPDATA%, so the two processes write/read different config files.
|
|
7
|
+
//
|
|
8
|
+
// Fix: On Windows, always build the path from %USERPROFILE% which is never sandboxed.
|
|
9
|
+
// On Mac/Linux Conf's default behavior is correct.
|
|
10
|
+
|
|
11
|
+
import os from "os";
|
|
12
|
+
import path from "path";
|
|
13
|
+
|
|
14
|
+
export function getConfOptions() {
|
|
15
|
+
if (process.platform === "win32") {
|
|
16
|
+
const userProfile = process.env.USERPROFILE || os.homedir();
|
|
17
|
+
const cwd = path.join(userProfile, "AppData", "Roaming", "clauth-nodejs", "Config");
|
|
18
|
+
return { projectName: "clauth", cwd };
|
|
19
|
+
}
|
|
20
|
+
return { projectName: "clauth" };
|
|
21
|
+
}
|
package/cli/fingerprint.js
CHANGED
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
// cli/fingerprint.js
|
|
2
|
-
// Collects stable hardware identifiers and derives HMAC tokens
|
|
3
|
-
// Works on Windows (primary) + Linux/macOS (fallback)
|
|
4
|
-
|
|
5
|
-
import { execSync } from "child_process";
|
|
6
|
-
import { createHmac, createHash } from "crypto";
|
|
7
|
-
import os from "os";
|
|
8
|
-
|
|
9
|
-
// ============================================================
|
|
10
|
-
// Machine ID collection
|
|
11
|
-
// ============================================================
|
|
12
|
-
|
|
13
|
-
function getMachineId() {
|
|
14
|
-
const platform = os.platform();
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
if (platform === "win32") {
|
|
18
|
-
// Primary: BIOS UUID via WMIC
|
|
19
|
-
const uuid = execSync("wmic csproduct get uuid /format:value", {
|
|
20
|
-
encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"]
|
|
21
|
-
}).match(/UUID=([A-F0-9-]+)/i)?.[1]?.trim();
|
|
22
|
-
|
|
23
|
-
// Secondary: Windows MachineGuid from registry
|
|
24
|
-
const machineGuid = execSync(
|
|
25
|
-
"reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid",
|
|
26
|
-
{ encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }
|
|
27
|
-
).match(/MachineGuid\s+REG_SZ\s+([a-f0-9-]+)/i)?.[1]?.trim();
|
|
28
|
-
|
|
29
|
-
if (!uuid || !machineGuid) throw new Error("Could not read Windows machine IDs");
|
|
30
|
-
return { primary: uuid, secondary: machineGuid, platform: "win32" };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (platform === "darwin") {
|
|
34
|
-
const uuid = execSync(
|
|
35
|
-
"ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/ { print $3 }'",
|
|
36
|
-
{ encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }
|
|
37
|
-
).replace(/['"]/g, "").trim();
|
|
38
|
-
return { primary: uuid, secondary: os.hostname(), platform: "darwin" };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Linux
|
|
42
|
-
let uuid = "";
|
|
43
|
-
try { uuid = execSync("cat /etc/machine-id", { encoding: "utf8", timeout: 2000 }).trim(); }
|
|
44
|
-
catch { uuid = execSync("cat /var/lib/dbus/machine-id", { encoding: "utf8", timeout: 2000 }).trim(); }
|
|
45
|
-
return { primary: uuid, secondary: os.hostname(), platform: "linux" };
|
|
46
|
-
|
|
47
|
-
} catch (err) {
|
|
48
|
-
throw new Error(`Machine ID collection failed: ${err.message}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ============================================================
|
|
53
|
-
// Derive stable machine hash (what gets stored in Supabase)
|
|
54
|
-
// ============================================================
|
|
55
|
-
|
|
56
|
-
export function getMachineHash() {
|
|
57
|
-
const { primary, secondary } = getMachineId();
|
|
58
|
-
return createHash("sha256")
|
|
59
|
-
.update(`${primary}:${secondary}`)
|
|
60
|
-
.digest("hex");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ============================================================
|
|
64
|
-
// Derive HMAC token for a given password + timestamp
|
|
65
|
-
// ============================================================
|
|
66
|
-
|
|
67
|
-
export function deriveToken(password, machineHash) {
|
|
68
|
-
const windowMs = 5 * 60 * 1000;
|
|
69
|
-
const window = Math.floor(Date.now() / windowMs);
|
|
70
|
-
const message = `${machineHash}:${window}`;
|
|
71
|
-
|
|
72
|
-
// Server reconstructs this — password + CLAUTH_HMAC_SALT
|
|
73
|
-
// Client sends token; server adds its SALT to the password before verifying
|
|
74
|
-
const token = createHmac("sha256", password)
|
|
75
|
-
.update(message)
|
|
76
|
-
.digest("hex");
|
|
77
|
-
|
|
78
|
-
return { token, timestamp: window * windowMs, machineHash };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ============================================================
|
|
82
|
-
// Derive HMAC seed hash (stored during machine registration)
|
|
83
|
-
// ============================================================
|
|
84
|
-
|
|
85
|
-
export function deriveSeedHash(machineHash, password) {
|
|
86
|
-
return createHash("sha256")
|
|
87
|
-
.update(`seed:${machineHash}:${password}`)
|
|
88
|
-
.digest("hex");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export default { getMachineHash, deriveToken, deriveSeedHash };
|
|
1
|
+
// cli/fingerprint.js
|
|
2
|
+
// Collects stable hardware identifiers and derives HMAC tokens
|
|
3
|
+
// Works on Windows (primary) + Linux/macOS (fallback)
|
|
4
|
+
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { createHmac, createHash } from "crypto";
|
|
7
|
+
import os from "os";
|
|
8
|
+
|
|
9
|
+
// ============================================================
|
|
10
|
+
// Machine ID collection
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
function getMachineId() {
|
|
14
|
+
const platform = os.platform();
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
if (platform === "win32") {
|
|
18
|
+
// Primary: BIOS UUID via WMIC
|
|
19
|
+
const uuid = execSync("wmic csproduct get uuid /format:value", {
|
|
20
|
+
encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"]
|
|
21
|
+
}).match(/UUID=([A-F0-9-]+)/i)?.[1]?.trim();
|
|
22
|
+
|
|
23
|
+
// Secondary: Windows MachineGuid from registry
|
|
24
|
+
const machineGuid = execSync(
|
|
25
|
+
"reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid",
|
|
26
|
+
{ encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }
|
|
27
|
+
).match(/MachineGuid\s+REG_SZ\s+([a-f0-9-]+)/i)?.[1]?.trim();
|
|
28
|
+
|
|
29
|
+
if (!uuid || !machineGuid) throw new Error("Could not read Windows machine IDs");
|
|
30
|
+
return { primary: uuid, secondary: machineGuid, platform: "win32" };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (platform === "darwin") {
|
|
34
|
+
const uuid = execSync(
|
|
35
|
+
"ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/ { print $3 }'",
|
|
36
|
+
{ encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }
|
|
37
|
+
).replace(/['"]/g, "").trim();
|
|
38
|
+
return { primary: uuid, secondary: os.hostname(), platform: "darwin" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Linux
|
|
42
|
+
let uuid = "";
|
|
43
|
+
try { uuid = execSync("cat /etc/machine-id", { encoding: "utf8", timeout: 2000 }).trim(); }
|
|
44
|
+
catch { uuid = execSync("cat /var/lib/dbus/machine-id", { encoding: "utf8", timeout: 2000 }).trim(); }
|
|
45
|
+
return { primary: uuid, secondary: os.hostname(), platform: "linux" };
|
|
46
|
+
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw new Error(`Machine ID collection failed: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================
|
|
53
|
+
// Derive stable machine hash (what gets stored in Supabase)
|
|
54
|
+
// ============================================================
|
|
55
|
+
|
|
56
|
+
export function getMachineHash() {
|
|
57
|
+
const { primary, secondary } = getMachineId();
|
|
58
|
+
return createHash("sha256")
|
|
59
|
+
.update(`${primary}:${secondary}`)
|
|
60
|
+
.digest("hex");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================
|
|
64
|
+
// Derive HMAC token for a given password + timestamp
|
|
65
|
+
// ============================================================
|
|
66
|
+
|
|
67
|
+
export function deriveToken(password, machineHash) {
|
|
68
|
+
const windowMs = 5 * 60 * 1000;
|
|
69
|
+
const window = Math.floor(Date.now() / windowMs);
|
|
70
|
+
const message = `${machineHash}:${window}`;
|
|
71
|
+
|
|
72
|
+
// Server reconstructs this — password + CLAUTH_HMAC_SALT
|
|
73
|
+
// Client sends token; server adds its SALT to the password before verifying
|
|
74
|
+
const token = createHmac("sha256", password)
|
|
75
|
+
.update(message)
|
|
76
|
+
.digest("hex");
|
|
77
|
+
|
|
78
|
+
return { token, timestamp: window * windowMs, machineHash };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================================
|
|
82
|
+
// Derive HMAC seed hash (stored during machine registration)
|
|
83
|
+
// ============================================================
|
|
84
|
+
|
|
85
|
+
export function deriveSeedHash(machineHash, password) {
|
|
86
|
+
return createHash("sha256")
|
|
87
|
+
.update(`seed:${machineHash}:${password}`)
|
|
88
|
+
.digest("hex");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default { getMachineHash, deriveToken, deriveSeedHash };
|
package/cli/index.js
CHANGED
|
@@ -6,12 +6,13 @@ import chalk from "chalk";
|
|
|
6
6
|
import ora from "ora";
|
|
7
7
|
import inquirer from "inquirer";
|
|
8
8
|
import Conf from "conf";
|
|
9
|
+
import { getConfOptions } from "./conf-path.js";
|
|
9
10
|
import { getMachineHash, deriveToken, deriveSeedHash } from "./fingerprint.js";
|
|
10
11
|
import * as api from "./api.js";
|
|
11
12
|
import os from "os";
|
|
12
13
|
|
|
13
|
-
const config = new Conf(
|
|
14
|
-
const VERSION = "0.
|
|
14
|
+
const config = new Conf(getConfOptions());
|
|
15
|
+
const VERSION = "0.4.0";
|
|
15
16
|
|
|
16
17
|
// ============================================================
|
|
17
18
|
// Password prompt helper
|
|
@@ -475,6 +476,7 @@ Actions:
|
|
|
475
476
|
restart Stop + start
|
|
476
477
|
ping Check if the daemon is running
|
|
477
478
|
foreground Run in foreground (Ctrl+C to stop) — default if no action given
|
|
479
|
+
mcp Run as MCP stdio server for Claude Code (JSON-RPC over stdin/stdout)
|
|
478
480
|
|
|
479
481
|
Examples:
|
|
480
482
|
clauth serve start Start locked — unlock at http://127.0.0.1:52437
|
|
@@ -483,6 +485,8 @@ Examples:
|
|
|
483
485
|
clauth serve ping Check status
|
|
484
486
|
clauth serve restart Restart (stays locked until browser unlock)
|
|
485
487
|
clauth serve start --services github,vercel
|
|
488
|
+
clauth serve mcp Start MCP server for Claude Code
|
|
489
|
+
clauth serve mcp -p mypass Start MCP server pre-unlocked
|
|
486
490
|
`)
|
|
487
491
|
.action(async (action, opts) => {
|
|
488
492
|
const resolvedAction = opts.action || action || "foreground";
|
package/install.ps1
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
# clauth installer — Windows
|
|
2
|
-
# One-liner: iex ((New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/LIFEAI/clauth/main/install.ps1'))
|
|
3
|
-
|
|
4
|
-
$ErrorActionPreference = "Stop"
|
|
5
|
-
$REPO = "https://github.com/LIFEAI/clauth.git"
|
|
6
|
-
$DIR = "$env:USERPROFILE\.clauth"
|
|
7
|
-
|
|
8
|
-
# Check git
|
|
9
|
-
try { git --version | Out-Null } catch {
|
|
10
|
-
Write-Host ""
|
|
11
|
-
Write-Host " x git is required." -ForegroundColor Red
|
|
12
|
-
Write-Host " Install from https://git-scm.com then re-run this script."
|
|
13
|
-
Write-Host ""
|
|
14
|
-
exit 1
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
# Check Node
|
|
18
|
-
try { node --version | Out-Null } catch {
|
|
19
|
-
Write-Host ""
|
|
20
|
-
Write-Host " x Node.js v18+ is required." -ForegroundColor Red
|
|
21
|
-
Write-Host " Install from https://nodejs.org then re-run this script."
|
|
22
|
-
Write-Host ""
|
|
23
|
-
exit 1
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
# Clone or update
|
|
27
|
-
if (Test-Path "$DIR\.git") {
|
|
28
|
-
Write-Host " Updating clauth..."
|
|
29
|
-
Set-Location $DIR; git pull --quiet
|
|
30
|
-
} else {
|
|
31
|
-
Write-Host " Cloning clauth..."
|
|
32
|
-
git clone --quiet $REPO $DIR
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
# Run compiled bootstrap binary
|
|
36
|
-
$bootstrap = "$DIR\scripts\bin\bootstrap-win.exe"
|
|
37
|
-
if (-not (Test-Path $bootstrap)) {
|
|
38
|
-
Write-Host " x Bootstrap binary not found at $bootstrap" -ForegroundColor Red
|
|
39
|
-
exit 1
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
Set-Location $DIR
|
|
43
|
-
& $bootstrap
|
|
44
|
-
exit $LASTEXITCODE
|
|
1
|
+
# clauth installer — Windows
|
|
2
|
+
# One-liner: iex ((New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/LIFEAI/clauth/main/install.ps1'))
|
|
3
|
+
|
|
4
|
+
$ErrorActionPreference = "Stop"
|
|
5
|
+
$REPO = "https://github.com/LIFEAI/clauth.git"
|
|
6
|
+
$DIR = "$env:USERPROFILE\.clauth"
|
|
7
|
+
|
|
8
|
+
# Check git
|
|
9
|
+
try { git --version | Out-Null } catch {
|
|
10
|
+
Write-Host ""
|
|
11
|
+
Write-Host " x git is required." -ForegroundColor Red
|
|
12
|
+
Write-Host " Install from https://git-scm.com then re-run this script."
|
|
13
|
+
Write-Host ""
|
|
14
|
+
exit 1
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Check Node
|
|
18
|
+
try { node --version | Out-Null } catch {
|
|
19
|
+
Write-Host ""
|
|
20
|
+
Write-Host " x Node.js v18+ is required." -ForegroundColor Red
|
|
21
|
+
Write-Host " Install from https://nodejs.org then re-run this script."
|
|
22
|
+
Write-Host ""
|
|
23
|
+
exit 1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Clone or update
|
|
27
|
+
if (Test-Path "$DIR\.git") {
|
|
28
|
+
Write-Host " Updating clauth..."
|
|
29
|
+
Set-Location $DIR; git pull --quiet
|
|
30
|
+
} else {
|
|
31
|
+
Write-Host " Cloning clauth..."
|
|
32
|
+
git clone --quiet $REPO $DIR
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Run compiled bootstrap binary
|
|
36
|
+
$bootstrap = "$DIR\scripts\bin\bootstrap-win.exe"
|
|
37
|
+
if (-not (Test-Path $bootstrap)) {
|
|
38
|
+
Write-Host " x Bootstrap binary not found at $bootstrap" -ForegroundColor Red
|
|
39
|
+
exit 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Set-Location $DIR
|
|
43
|
+
& $bootstrap
|
|
44
|
+
exit $LASTEXITCODE
|