@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/.clauth-skill/SKILL.md +141 -0
- package/.clauth-skill/references/keys-guide.md +270 -0
- package/.clauth-skill/references/operator-guide.md +148 -0
- package/README.md +101 -0
- package/cli/api.js +108 -0
- package/cli/commands/install.js +258 -0
- package/cli/fingerprint.js +91 -0
- package/cli/index.js +403 -0
- package/install.ps1 +44 -0
- package/install.sh +38 -0
- package/package.json +54 -0
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bin/bootstrap-win.exe +0 -0
- package/scripts/bootstrap.cjs +43 -0
- package/scripts/build.sh +45 -0
- package/supabase/functions/auth-vault/index.ts +326 -0
- package/supabase/migrations/001_clauth_schema.sql +94 -0
- package/supabase/migrations/002_vault_helpers.sql +90 -0
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 };
|