@lifeaitools/clauth 0.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/cli/api.js ADDED
@@ -0,0 +1,108 @@
1
+ // cli/api.js
2
+ // Thin client that calls the auth-vault Edge Function
3
+
4
+ import { createRequire } from "module";
5
+ const require = createRequire(import.meta.url);
6
+
7
+ import Conf from "conf";
8
+
9
+ const config = new Conf({ projectName: "clauth" });
10
+
11
+ // ============================================================
12
+ // Get Edge Function base URL from local config
13
+ // ============================================================
14
+ export function getBaseUrl() {
15
+ const url = config.get("supabase_url") || process.env.CLAUTH_SUPABASE_URL;
16
+ if (!url) throw new Error("Supabase URL not configured. Run: clauth setup");
17
+ return `${url}/functions/v1/auth-vault`;
18
+ }
19
+
20
+ export function getAnonKey() {
21
+ const key = config.get("supabase_anon_key") || process.env.CLAUTH_SUPABASE_ANON_KEY;
22
+ if (!key) throw new Error("Supabase anon key not configured. Run: clauth setup");
23
+ return key;
24
+ }
25
+
26
+ // ============================================================
27
+ // Core POST helper
28
+ // ============================================================
29
+ async function post(route, body) {
30
+ const url = `${getBaseUrl()}/${route}`;
31
+ const anonKey = getAnonKey();
32
+
33
+ const res = await fetch(url, {
34
+ method: "POST",
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ "Authorization": `Bearer ${anonKey}`
38
+ },
39
+ body: JSON.stringify(body)
40
+ });
41
+
42
+ const data = await res.json();
43
+ if (!res.ok && !data.error) throw new Error(`HTTP ${res.status}`);
44
+ return data;
45
+ }
46
+
47
+ // ============================================================
48
+ // Auth-bearing calls (require HMAC token)
49
+ // ============================================================
50
+ async function authPost(route, password, machineHash, token, timestamp, extra = {}) {
51
+ return post(route, {
52
+ machine_hash: machineHash,
53
+ token,
54
+ timestamp,
55
+ password,
56
+ ...extra
57
+ });
58
+ }
59
+
60
+ // ============================================================
61
+ // Exported API surface
62
+ // ============================================================
63
+
64
+ export async function retrieve(password, machineHash, token, timestamp, service) {
65
+ return authPost("retrieve", password, machineHash, token, timestamp, { service });
66
+ }
67
+
68
+ export async function write(password, machineHash, token, timestamp, service, value) {
69
+ return authPost("write", password, machineHash, token, timestamp, { service, value });
70
+ }
71
+
72
+ export async function enable(password, machineHash, token, timestamp, service, enabled) {
73
+ return authPost("enable", password, machineHash, token, timestamp, { service, enabled });
74
+ }
75
+
76
+ export async function addService(password, machineHash, token, timestamp, name, label, key_type, description) {
77
+ return authPost("add", password, machineHash, token, timestamp, { name, label, key_type, description });
78
+ }
79
+
80
+ export async function removeService(password, machineHash, token, timestamp, service, confirm) {
81
+ return authPost("remove", password, machineHash, token, timestamp, { service, confirm });
82
+ }
83
+
84
+ export async function revoke(password, machineHash, token, timestamp, service, confirm) {
85
+ return authPost("revoke", password, machineHash, token, timestamp, { service, confirm });
86
+ }
87
+
88
+ export async function status(password, machineHash, token, timestamp) {
89
+ return authPost("status", password, machineHash, token, timestamp);
90
+ }
91
+
92
+ export async function test(password, machineHash, token, timestamp) {
93
+ return authPost("test", password, machineHash, token, timestamp);
94
+ }
95
+
96
+ export async function registerMachine(machineHash, seedHash, label, adminToken) {
97
+ return post("register-machine", {
98
+ machine_hash: machineHash,
99
+ hmac_seed_hash: seedHash,
100
+ label,
101
+ admin_token: adminToken
102
+ });
103
+ }
104
+
105
+ export default {
106
+ retrieve, write, enable, addService, removeService, revoke,
107
+ status, test, registerMachine, getBaseUrl, getAnonKey
108
+ };
@@ -0,0 +1,258 @@
1
+ // cli/commands/install.js
2
+ // clauth install — full provisioning command
3
+ // Called automatically by the bootstrap installer, or manually: clauth install
4
+ //
5
+ // Does:
6
+ // 1. Checks prerequisites (git, node, internet)
7
+ // 2. Collects Supabase project ref + PAT
8
+ // 3. Runs SQL migrations
9
+ // 4. Deploys auth-vault Edge Function
10
+ // 5. Generates HMAC salt + bootstrap token, stores as project secrets
11
+ // 6. Saves local config (Supabase URL + anon key)
12
+ // 7. Runs end-to-end test
13
+ // 8. Installs Claude skill
14
+ // 9. Cleans up
15
+ // 10. Prints: "clauth ready. Type clauth --help"
16
+
17
+ import { createInterface } from 'readline';
18
+ import { randomBytes } from 'crypto';
19
+ import { readFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'fs';
20
+ import { join, dirname } from 'path';
21
+ import { fileURLToPath } from 'url';
22
+ import { execSync } from 'child_process';
23
+ import Conf from 'conf';
24
+ import chalk from 'chalk';
25
+ import ora from 'ora';
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const ROOT = join(__dirname, '..', '..');
29
+ const MGMT = 'https://api.supabase.com/v1';
30
+ const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
31
+ (process.platform === 'win32'
32
+ ? join(process.env.USERPROFILE || '', '.claude', 'skills')
33
+ : join(process.env.HOME || '', '.claude', 'skills'));
34
+
35
+ // ─────────────────────────────────────────────
36
+ // Prompt helpers
37
+ // ─────────────────────────────────────────────
38
+
39
+ function ask(question) {
40
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
41
+ return new Promise(res => rl.question(question, a => { rl.close(); res(a.trim()); }));
42
+ }
43
+
44
+ function askSecret(label) {
45
+ return new Promise(res => {
46
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
47
+ process.stdout.write(label);
48
+ let val = '';
49
+ if (process.stdin.isTTY) {
50
+ process.stdin.setRawMode(true);
51
+ process.stdin.resume();
52
+ process.stdin.setEncoding('utf8');
53
+ const onData = ch => {
54
+ if (ch === '\r' || ch === '\n') {
55
+ process.stdin.setRawMode(false);
56
+ process.stdin.pause();
57
+ process.stdin.removeListener('data', onData);
58
+ process.stdout.write('\n');
59
+ rl.close();
60
+ res(val);
61
+ } else if (ch === '\u0003') {
62
+ process.exit();
63
+ } else if (ch === '\u007f') {
64
+ val = val.slice(0, -1);
65
+ } else {
66
+ val += ch;
67
+ process.stdout.write('*');
68
+ }
69
+ };
70
+ process.stdin.on('data', onData);
71
+ } else {
72
+ rl.question('', a => { rl.close(); res(a.trim()); });
73
+ }
74
+ });
75
+ }
76
+
77
+ // ─────────────────────────────────────────────
78
+ // Supabase Management API
79
+ // ─────────────────────────────────────────────
80
+
81
+ async function mgmt(pat, method, path, body) {
82
+ const res = await fetch(`${MGMT}${path}`, {
83
+ method,
84
+ headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
85
+ body: body ? JSON.stringify(body) : undefined,
86
+ });
87
+ if (!res.ok) {
88
+ const text = await res.text().catch(() => res.statusText);
89
+ throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
90
+ }
91
+ if (res.status === 204) return {};
92
+ return res.json();
93
+ }
94
+
95
+ // ─────────────────────────────────────────────
96
+ // Main install command
97
+ // ─────────────────────────────────────────────
98
+
99
+ export async function runInstall() {
100
+ console.log(chalk.cyan('\n🔐 clauth install\n'));
101
+
102
+ // ── Step 1: Check internet ────────────────
103
+ const s1 = ora('Checking connectivity...').start();
104
+ try {
105
+ await fetch('https://api.supabase.com/v1/projects', {
106
+ method: 'GET', headers: { 'Authorization': 'Bearer test' }
107
+ });
108
+ s1.succeed('Connected');
109
+ } catch {
110
+ s1.fail('No internet connection. Check your network and retry.');
111
+ process.exit(1);
112
+ }
113
+
114
+ // ── Step 2: Collect credentials ───────────
115
+ console.log(chalk.cyan('\nYou need two things from Supabase:\n'));
116
+ console.log(chalk.gray(' Project ref: last segment of your project URL'));
117
+ console.log(chalk.gray(' e.g. supabase.com/dashboard/project/') + chalk.white('uvojezuorjgqzmhhgluu'));
118
+ console.log('');
119
+ console.log(chalk.gray(' Personal Access Token (PAT):'));
120
+ console.log(chalk.gray(' supabase.com/dashboard/account/tokens → Generate new token'));
121
+ console.log(chalk.gray(' (This is NOT your anon key or service_role key)\n'));
122
+
123
+ const ref = await ask(chalk.white('Supabase project ref: '));
124
+ const pat = await askSecret(chalk.white('Supabase PAT: '));
125
+
126
+ if (!ref || !pat) {
127
+ console.log(chalk.red('\n✗ Both required.\n')); process.exit(1);
128
+ }
129
+
130
+ // ── Step 3: Verify + fetch keys ──────────
131
+ const s3 = ora('Verifying Supabase credentials...').start();
132
+ let anon, serviceRole;
133
+ try {
134
+ const keys = await mgmt(pat, 'GET', `/projects/${ref}/api-keys`);
135
+ anon = keys.find(k => k.name === 'anon')?.api_key;
136
+ serviceRole = keys.find(k => k.name === 'service_role')?.api_key;
137
+ if (!anon || !serviceRole) throw new Error('API keys not found in response');
138
+ s3.succeed('Supabase project verified');
139
+ } catch (e) {
140
+ s3.fail(`Could not connect: ${e.message}`);
141
+ console.log(chalk.gray(' Check your project ref and PAT.'));
142
+ process.exit(1);
143
+ }
144
+
145
+ const projectUrl = `https://${ref}.supabase.co`;
146
+
147
+ // ── Step 4: Run migrations ────────────────
148
+ const s4 = ora('Running database migrations...').start();
149
+ const migrations = [
150
+ { name: '001_clauth_schema', file: 'supabase/migrations/001_clauth_schema.sql' },
151
+ { name: '002_vault_helpers', file: 'supabase/migrations/002_vault_helpers.sql' },
152
+ ];
153
+ for (const m of migrations) {
154
+ const sql = readFileSync(join(ROOT, m.file), 'utf8');
155
+ try {
156
+ await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: sql });
157
+ s4.text = `Migrations: ${m.name} ✓`;
158
+ } catch (e) {
159
+ s4.fail(`Migration ${m.name} failed: ${e.message}`);
160
+ process.exit(1);
161
+ }
162
+ }
163
+ s4.succeed('Database migrations applied');
164
+
165
+ // ── Step 5: Deploy Edge Function ─────────
166
+ const s5 = ora('Deploying auth-vault Edge Function...').start();
167
+ const fnSource = readFileSync(join(ROOT, 'supabase/functions/auth-vault/index.ts'), 'utf8');
168
+ try {
169
+ // Try update first
170
+ try {
171
+ await mgmt(pat, 'PATCH', `/projects/${ref}/functions/auth-vault`, {
172
+ slug: 'auth-vault', name: 'auth-vault', verify_jwt: true, body: fnSource,
173
+ });
174
+ } catch {
175
+ await mgmt(pat, 'POST', `/projects/${ref}/functions`, {
176
+ slug: 'auth-vault', name: 'auth-vault', verify_jwt: true, body: fnSource,
177
+ });
178
+ }
179
+ s5.succeed('auth-vault Edge Function deployed');
180
+ } catch (e) {
181
+ s5.warn(`Edge Function deploy failed: ${e.message}`);
182
+ console.log(chalk.yellow(' Run manually: supabase functions deploy auth-vault'));
183
+ }
184
+
185
+ // ── Step 6: Generate + store secrets ─────
186
+ const s6 = ora('Generating secrets...').start();
187
+ const hmacSalt = randomBytes(32).toString('hex');
188
+ const bootstrapToken = randomBytes(16).toString('hex');
189
+ try {
190
+ await mgmt(pat, 'POST', `/projects/${ref}/secrets`, [
191
+ { name: 'CLAUTH_HMAC_SALT', value: hmacSalt },
192
+ { name: 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN', value: bootstrapToken },
193
+ ]);
194
+ s6.succeed('Secrets generated and stored');
195
+ } catch (e) {
196
+ s6.warn(`Could not store secrets via API: ${e.message}`);
197
+ console.log(chalk.yellow('\n Set these manually in Supabase → Settings → Edge Functions → Secrets:'));
198
+ console.log(chalk.white(` CLAUTH_HMAC_SALT = ${hmacSalt}`));
199
+ console.log(chalk.white(` CLAUTH_ADMIN_BOOTSTRAP_TOKEN = ${bootstrapToken}\n`));
200
+ }
201
+
202
+ // ── Step 7: Save local config ─────────────
203
+ const config = new Conf({ projectName: 'clauth' });
204
+ config.set('supabase_url', projectUrl);
205
+ config.set('supabase_anon_key', anon);
206
+
207
+ // ── Step 8: End-to-end test ───────────────
208
+ const s8 = ora('Running end-to-end test...').start();
209
+ try {
210
+ // Register a test machine
211
+ const testMachine = `install-test-${randomBytes(4).toString('hex')}`;
212
+ const regRes = await fetch(`${projectUrl}/functions/v1/auth-vault/register-machine`, {
213
+ method: 'POST',
214
+ headers: { 'Authorization': `Bearer ${anon}`, 'Content-Type': 'application/json' },
215
+ body: JSON.stringify({
216
+ machine_hash: testMachine, hmac_seed_hash: 'test',
217
+ label: 'install-test', admin_token: bootstrapToken,
218
+ }),
219
+ });
220
+ const reg = await regRes.json();
221
+ if (!reg.success) throw new Error(`register-machine: ${JSON.stringify(reg)}`);
222
+ s8.succeed('End-to-end test passed — Edge Function is live');
223
+ } catch (e) {
224
+ s8.warn(`Test failed (Edge Function may still be deploying): ${e.message}`);
225
+ console.log(chalk.yellow(' Run clauth test after a minute to verify.'));
226
+ }
227
+
228
+ // ── Step 9: Install Claude skill ──────────
229
+ const s9 = ora('Installing Claude skill...').start();
230
+ const skillSrc = join(ROOT, '.clauth-skill');
231
+ if (existsSync(skillSrc)) {
232
+ try {
233
+ const skillDest = join(SKILLS_DIR, 'clauth');
234
+ mkdirSync(skillDest, { recursive: true });
235
+ cpSync(skillSrc, skillDest, { recursive: true, force: true });
236
+ s9.succeed(`Claude skill installed → ${skillDest}`);
237
+ } catch (e) {
238
+ s9.warn(`Skill install skipped: ${e.message}`);
239
+ }
240
+ } else {
241
+ s9.warn('Skill directory not found — skipping');
242
+ }
243
+
244
+ // ── Step 10: Done ─────────────────────────
245
+ console.log('');
246
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
247
+ console.log(chalk.green(' ✓ clauth installed and ready'));
248
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
249
+ console.log('');
250
+ console.log(chalk.yellow(' Save this bootstrap token — you need it once for clauth setup:'));
251
+ console.log(chalk.white(' ' + bootstrapToken));
252
+ console.log('');
253
+ console.log(chalk.gray(' (Also stored in Supabase → Settings → Edge Functions → Secrets)'));
254
+ console.log('');
255
+ console.log(chalk.white(' Next: run') + chalk.cyan(' clauth setup'));
256
+ console.log(chalk.gray(' Then:') + chalk.cyan(' clauth test'));
257
+ console.log('');
258
+ }
@@ -0,0 +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 };