@lifeaitools/clauth 0.7.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/api.js +13 -5
- package/cli/commands/doctor.js +302 -0
- package/cli/commands/install.js +113 -0
- package/cli/commands/invite.js +175 -0
- package/cli/commands/join.js +179 -0
- package/cli/commands/serve.js +1135 -204
- package/cli/commands/tunnel.js +210 -0
- package/cli/index.js +140 -16
- package/install.ps1 +7 -0
- package/install.sh +11 -0
- package/package.json +7 -2
- package/scripts/bootstrap.cjs +78 -0
- package/scripts/postinstall.js +52 -0
- package/supabase/functions/auth-vault/index.ts +27 -7
- package/supabase/migrations/003_clauth_config.sql +13 -0
package/cli/api.js
CHANGED
|
@@ -74,8 +74,14 @@ export async function enable(password, machineHash, token, timestamp, service, e
|
|
|
74
74
|
return authPost("enable", password, machineHash, token, timestamp, { service, enabled });
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export async function addService(password, machineHash, token, timestamp, name, label, key_type, description) {
|
|
78
|
-
|
|
77
|
+
export async function addService(password, machineHash, token, timestamp, name, label, key_type, description, project) {
|
|
78
|
+
const extra = { name, label, key_type, description };
|
|
79
|
+
if (project) extra.project = project;
|
|
80
|
+
return authPost("add", password, machineHash, token, timestamp, extra);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function updateService(password, machineHash, token, timestamp, service, updates) {
|
|
84
|
+
return authPost("update", password, machineHash, token, timestamp, { service, ...updates });
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
export async function removeService(password, machineHash, token, timestamp, service, confirm) {
|
|
@@ -86,8 +92,10 @@ export async function revoke(password, machineHash, token, timestamp, service, c
|
|
|
86
92
|
return authPost("revoke", password, machineHash, token, timestamp, { service, confirm });
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
export async function status(password, machineHash, token, timestamp) {
|
|
90
|
-
|
|
95
|
+
export async function status(password, machineHash, token, timestamp, project) {
|
|
96
|
+
const extra = {};
|
|
97
|
+
if (project) extra.project = project;
|
|
98
|
+
return authPost("status", password, machineHash, token, timestamp, extra);
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
export async function test(password, machineHash, token, timestamp) {
|
|
@@ -108,6 +116,6 @@ export async function registerMachine(machineHash, seedHash, label, adminToken)
|
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
export default {
|
|
111
|
-
retrieve, write, enable, addService, removeService, revoke,
|
|
119
|
+
retrieve, write, enable, addService, updateService, removeService, revoke,
|
|
112
120
|
status, test, registerMachine, getBaseUrl, getAnonKey
|
|
113
121
|
};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// cli/commands/doctor.js
|
|
2
|
+
// clauth doctor — check all prerequisites for a healthy installation
|
|
3
|
+
// Checks: Node version, cloudflared, network, Supabase connectivity, config, service status
|
|
4
|
+
|
|
5
|
+
import os from "os";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import ora from "ora";
|
|
12
|
+
import Conf from "conf";
|
|
13
|
+
import { getConfOptions } from "../conf-path.js";
|
|
14
|
+
import { getMachineHash } from "../fingerprint.js";
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
|
|
18
|
+
|
|
19
|
+
const CHECK = chalk.green("\u2713");
|
|
20
|
+
const CROSS = chalk.red("\u2717");
|
|
21
|
+
const WARN = chalk.yellow("!");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run a shell command and return stdout, or null on failure.
|
|
25
|
+
*/
|
|
26
|
+
function execSafe(cmd, timeoutMs = 10000) {
|
|
27
|
+
try {
|
|
28
|
+
return execSync(cmd, {
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
timeout: timeoutMs,
|
|
31
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
32
|
+
}).trim();
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runDoctor() {
|
|
39
|
+
const results = [];
|
|
40
|
+
const platform = os.platform();
|
|
41
|
+
|
|
42
|
+
console.log(chalk.cyan(`\n clauth doctor v${pkg.version}\n`));
|
|
43
|
+
console.log(chalk.gray(` Platform: ${platform} (${os.arch()})`));
|
|
44
|
+
console.log(chalk.gray(` Node: ${process.version}`));
|
|
45
|
+
console.log();
|
|
46
|
+
|
|
47
|
+
// ── 1. Node version ──────────────────────────────────────
|
|
48
|
+
{
|
|
49
|
+
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
50
|
+
if (major >= 18) {
|
|
51
|
+
results.push(true);
|
|
52
|
+
console.log(` ${CHECK} Node.js ${process.versions.node} (>= 18 required)`);
|
|
53
|
+
} else {
|
|
54
|
+
results.push(false);
|
|
55
|
+
console.log(` ${CROSS} Node.js ${process.versions.node} — version 18+ required`);
|
|
56
|
+
console.log(chalk.gray(" Fix: Install Node.js 18+ from https://nodejs.org/"));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── 2. cloudflared ───────────────────────────────────────
|
|
61
|
+
{
|
|
62
|
+
// Check common Windows paths if not on PATH
|
|
63
|
+
let cfVersion = execSafe("cloudflared --version");
|
|
64
|
+
if (!cfVersion && platform === "win32") {
|
|
65
|
+
const winPaths = [
|
|
66
|
+
path.join(process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)", "cloudflared", "cloudflared.exe"),
|
|
67
|
+
path.join(process.env.ProgramFiles || "C:\\Program Files", "cloudflared", "cloudflared.exe"),
|
|
68
|
+
path.join(os.homedir(), "scoop", "shims", "cloudflared.exe"),
|
|
69
|
+
"C:\\ProgramData\\chocolatey\\bin\\cloudflared.exe",
|
|
70
|
+
];
|
|
71
|
+
for (const p of winPaths) {
|
|
72
|
+
try { if (fs.statSync(p).isFile()) { cfVersion = execSafe(`"${p}" --version`); break; } } catch {}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (cfVersion) {
|
|
76
|
+
results.push(true);
|
|
77
|
+
console.log(` ${CHECK} cloudflared installed (${cfVersion.split("\n")[0]})`);
|
|
78
|
+
} else {
|
|
79
|
+
results.push(false);
|
|
80
|
+
console.log(` ${CROSS} cloudflared not found`);
|
|
81
|
+
if (platform === "win32") {
|
|
82
|
+
console.log(chalk.gray(" Fix: winget install Cloudflare.cloudflared"));
|
|
83
|
+
console.log(chalk.gray(" or: choco install cloudflared -y"));
|
|
84
|
+
} else if (platform === "darwin") {
|
|
85
|
+
console.log(chalk.gray(" Fix: brew install cloudflared"));
|
|
86
|
+
} else {
|
|
87
|
+
console.log(chalk.gray(" Fix: curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared"));
|
|
88
|
+
console.log(chalk.gray(" or: snap install cloudflared"));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── 3. Config exists ─────────────────────────────────────
|
|
94
|
+
{
|
|
95
|
+
const config = new Conf(getConfOptions());
|
|
96
|
+
const supaUrl = config.get("supabase_url");
|
|
97
|
+
const anonKey = config.get("supabase_anon_key");
|
|
98
|
+
|
|
99
|
+
if (supaUrl && anonKey) {
|
|
100
|
+
results.push(true);
|
|
101
|
+
console.log(` ${CHECK} Config found (Supabase URL + anon key saved)`);
|
|
102
|
+
console.log(chalk.gray(` URL: ${supaUrl}`));
|
|
103
|
+
} else if (supaUrl && !anonKey) {
|
|
104
|
+
results.push(false);
|
|
105
|
+
console.log(` ${CROSS} Config incomplete — Supabase anon key missing`);
|
|
106
|
+
console.log(chalk.gray(" Fix: Run 'clauth install' to complete setup"));
|
|
107
|
+
} else {
|
|
108
|
+
results.push(false);
|
|
109
|
+
console.log(` ${CROSS} Config not found — clauth not installed`);
|
|
110
|
+
console.log(chalk.gray(" Fix: Run 'clauth install' to set up your vault"));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── 4. Network: can reach Supabase ───────────────────────
|
|
115
|
+
{
|
|
116
|
+
const config = new Conf(getConfOptions());
|
|
117
|
+
const supaUrl = config.get("supabase_url");
|
|
118
|
+
|
|
119
|
+
if (supaUrl) {
|
|
120
|
+
try {
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
123
|
+
const res = await fetch(`${supaUrl}/rest/v1/`, {
|
|
124
|
+
method: "HEAD",
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
headers: { apikey: config.get("supabase_anon_key") || "" },
|
|
127
|
+
});
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
if (res.ok || res.status === 401 || res.status === 400) {
|
|
130
|
+
results.push(true);
|
|
131
|
+
console.log(` ${CHECK} Network — Supabase reachable (HTTP ${res.status})`);
|
|
132
|
+
} else {
|
|
133
|
+
results.push(false);
|
|
134
|
+
console.log(` ${CROSS} Network — Supabase returned HTTP ${res.status}`);
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
results.push(false);
|
|
138
|
+
console.log(` ${CROSS} Network — cannot reach Supabase: ${err.message}`);
|
|
139
|
+
console.log(chalk.gray(" Check your internet connection"));
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
results.push(null);
|
|
143
|
+
console.log(` ${WARN} Network — skipped (no Supabase URL configured)`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── 5. Machine fingerprint ───────────────────────────────
|
|
148
|
+
{
|
|
149
|
+
try {
|
|
150
|
+
const hash = getMachineHash();
|
|
151
|
+
results.push(true);
|
|
152
|
+
console.log(` ${CHECK} Machine fingerprint: ${hash.slice(0, 16)}...`);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
results.push(false);
|
|
155
|
+
console.log(` ${CROSS} Machine fingerprint failed: ${err.message}`);
|
|
156
|
+
console.log(chalk.gray(" Fix: Run 'clauth setup' to register this machine"));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── 6. Daemon status ─────────────────────────────────────
|
|
161
|
+
{
|
|
162
|
+
try {
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
165
|
+
const res = await fetch("http://127.0.0.1:52437/ping", { signal: controller.signal });
|
|
166
|
+
clearTimeout(timeout);
|
|
167
|
+
if (res.ok) {
|
|
168
|
+
const data = await res.json();
|
|
169
|
+
results.push(true);
|
|
170
|
+
const locked = data.locked ? chalk.yellow(" (locked)") : chalk.green(" (unlocked)");
|
|
171
|
+
console.log(` ${CHECK} Daemon running on port ${data.port || 52437}${locked}`);
|
|
172
|
+
} else {
|
|
173
|
+
results.push(false);
|
|
174
|
+
console.log(` ${CROSS} Daemon responded with HTTP ${res.status}`);
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
results.push(false);
|
|
178
|
+
console.log(` ${CROSS} Daemon not running`);
|
|
179
|
+
console.log(chalk.gray(" Fix: clauth serve start"));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── 7. Autostart / service registration ──────────────────
|
|
184
|
+
{
|
|
185
|
+
if (platform === "win32") {
|
|
186
|
+
const taskOutput = execSafe('schtasks /query /tn "ClauthAutostart" 2>&1');
|
|
187
|
+
if (taskOutput && !taskOutput.includes("ERROR")) {
|
|
188
|
+
results.push(true);
|
|
189
|
+
console.log(` ${CHECK} Windows Scheduled Task registered (ClauthAutostart)`);
|
|
190
|
+
} else {
|
|
191
|
+
results.push(null);
|
|
192
|
+
console.log(` ${WARN} Windows Scheduled Task not registered`);
|
|
193
|
+
console.log(chalk.gray(" Optional: clauth serve install (auto-start on login)"));
|
|
194
|
+
}
|
|
195
|
+
} else if (platform === "darwin") {
|
|
196
|
+
const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", "com.lifeai.clauth.plist");
|
|
197
|
+
if (fs.existsSync(plistPath)) {
|
|
198
|
+
results.push(true);
|
|
199
|
+
console.log(` ${CHECK} macOS LaunchAgent registered`);
|
|
200
|
+
} else {
|
|
201
|
+
results.push(null);
|
|
202
|
+
console.log(` ${WARN} macOS LaunchAgent not registered`);
|
|
203
|
+
console.log(chalk.gray(" Optional: clauth serve install (auto-start on login)"));
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
const serviceFile = path.join(os.homedir(), ".config", "systemd", "user", "clauth.service");
|
|
207
|
+
if (fs.existsSync(serviceFile)) {
|
|
208
|
+
results.push(true);
|
|
209
|
+
console.log(` ${CHECK} systemd user service registered`);
|
|
210
|
+
} else {
|
|
211
|
+
results.push(null);
|
|
212
|
+
console.log(` ${WARN} systemd user service not registered`);
|
|
213
|
+
console.log(chalk.gray(" Optional: clauth serve install (auto-start on login)"));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── 8. Tunnel configuration ──────────────────────────────
|
|
219
|
+
{
|
|
220
|
+
const config = new Conf(getConfOptions());
|
|
221
|
+
const tunnelHostname = config.get("tunnel_hostname");
|
|
222
|
+
if (tunnelHostname) {
|
|
223
|
+
results.push(true);
|
|
224
|
+
console.log(` ${CHECK} Tunnel configured: ${tunnelHostname}`);
|
|
225
|
+
} else {
|
|
226
|
+
results.push(null);
|
|
227
|
+
console.log(` ${WARN} Tunnel not configured (optional for remote MCP access)`);
|
|
228
|
+
console.log(chalk.gray(" Optional: clauth serve install --tunnel <hostname>"));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Summary ──────────────────────────────────────────────
|
|
233
|
+
console.log();
|
|
234
|
+
const failed = results.filter(r => r === false).length;
|
|
235
|
+
const warnings = results.filter(r => r === null).length;
|
|
236
|
+
const passed = results.filter(r => r === true).length;
|
|
237
|
+
|
|
238
|
+
if (failed === 0) {
|
|
239
|
+
console.log(chalk.green(` All ${passed} checks passed.`));
|
|
240
|
+
if (warnings > 0) {
|
|
241
|
+
console.log(chalk.yellow(` ${warnings} optional recommendation${warnings > 1 ? "s" : ""}.`));
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
console.log(chalk.red(` ${failed} issue${failed > 1 ? "s" : ""} found.`));
|
|
245
|
+
if (passed > 0) console.log(chalk.green(` ${passed} check${passed > 1 ? "s" : ""} passed.`));
|
|
246
|
+
if (warnings > 0) console.log(chalk.yellow(` ${warnings} optional.`));
|
|
247
|
+
console.log(chalk.gray("\n Run the suggested fix commands above to resolve issues."));
|
|
248
|
+
}
|
|
249
|
+
console.log();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Install cloudflared on the current platform.
|
|
254
|
+
* Exported so other commands (e.g., serve install) can offer auto-install.
|
|
255
|
+
*/
|
|
256
|
+
export async function installCloudflared() {
|
|
257
|
+
const platform = os.platform();
|
|
258
|
+
const spinner = ora("Installing cloudflared...").start();
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
if (platform === "win32") {
|
|
262
|
+
// Try winget first
|
|
263
|
+
try {
|
|
264
|
+
execSync(
|
|
265
|
+
"winget install Cloudflare.cloudflared --accept-source-agreements --accept-package-agreements",
|
|
266
|
+
{ stdio: "inherit", timeout: 120000 }
|
|
267
|
+
);
|
|
268
|
+
spinner.succeed(chalk.green("cloudflared installed via winget"));
|
|
269
|
+
return true;
|
|
270
|
+
} catch {
|
|
271
|
+
spinner.text = "winget failed, trying chocolatey...";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Try chocolatey
|
|
275
|
+
try {
|
|
276
|
+
execSync("choco install cloudflared -y", { stdio: "inherit", timeout: 120000 });
|
|
277
|
+
spinner.succeed(chalk.green("cloudflared installed via chocolatey"));
|
|
278
|
+
return true;
|
|
279
|
+
} catch {
|
|
280
|
+
spinner.fail(chalk.red("Could not install cloudflared automatically"));
|
|
281
|
+
console.log(chalk.gray(" Download manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"));
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
} else if (platform === "darwin") {
|
|
285
|
+
execSync("brew install cloudflared", { stdio: "inherit", timeout: 120000 });
|
|
286
|
+
spinner.succeed(chalk.green("cloudflared installed via Homebrew"));
|
|
287
|
+
return true;
|
|
288
|
+
} else {
|
|
289
|
+
// Linux
|
|
290
|
+
execSync(
|
|
291
|
+
"curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared",
|
|
292
|
+
{ stdio: "inherit", timeout: 120000 }
|
|
293
|
+
);
|
|
294
|
+
spinner.succeed(chalk.green("cloudflared installed"));
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
} catch (err) {
|
|
298
|
+
spinner.fail(chalk.red(`cloudflared installation failed: ${err.message}`));
|
|
299
|
+
console.log(chalk.gray(" Install manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"));
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
package/cli/commands/install.js
CHANGED
|
@@ -85,6 +85,119 @@ async function mgmt(pat, method, path, body) {
|
|
|
85
85
|
export async function runInstall(opts = {}) {
|
|
86
86
|
console.log(chalk.cyan('\n🔐 clauth install\n'));
|
|
87
87
|
|
|
88
|
+
// ── Step 0: Upgrade detection ──────────────
|
|
89
|
+
const config0 = new Conf(getConfOptions());
|
|
90
|
+
if (config0.get('supabase_url')) {
|
|
91
|
+
console.log(chalk.cyan(' Existing installation detected.\n'));
|
|
92
|
+
console.log(chalk.gray(` Vault: ${config0.get('supabase_url')}`));
|
|
93
|
+
console.log(chalk.gray(` Config: ${config0.path}\n`));
|
|
94
|
+
|
|
95
|
+
const inquirerMod = await import('inquirer');
|
|
96
|
+
const { action } = await inquirerMod.default.prompt([{
|
|
97
|
+
type: 'list',
|
|
98
|
+
name: 'action',
|
|
99
|
+
message: 'What would you like to do?',
|
|
100
|
+
choices: [
|
|
101
|
+
{ name: 'Upgrade (keep config, redeploy Edge Function + migrations)', value: 'upgrade' },
|
|
102
|
+
{ name: 'Fresh install (new vault)', value: 'fresh' },
|
|
103
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
104
|
+
],
|
|
105
|
+
}]);
|
|
106
|
+
|
|
107
|
+
if (action === 'cancel') {
|
|
108
|
+
console.log(chalk.gray('\n Cancelled.\n'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (action === 'upgrade') {
|
|
113
|
+
// Upgrade path: skip provisioning, just redeploy Edge Function and run migrations
|
|
114
|
+
console.log(chalk.cyan('\n Running upgrade...\n'));
|
|
115
|
+
|
|
116
|
+
let ref = opts.ref;
|
|
117
|
+
let pat = opts.pat;
|
|
118
|
+
if (!ref || !pat) {
|
|
119
|
+
console.log(chalk.gray(' Need Supabase PAT to redeploy Edge Function + run migrations.\n'));
|
|
120
|
+
if (!ref) ref = await ask(chalk.white('Supabase project ref: '));
|
|
121
|
+
if (!pat) pat = await askSecret(chalk.white('Supabase PAT: '));
|
|
122
|
+
closeRL();
|
|
123
|
+
}
|
|
124
|
+
if (!ref || !pat) {
|
|
125
|
+
console.log(chalk.red('\n Both required.\n')); process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Run migrations
|
|
129
|
+
const s4 = ora('Running database migrations...').start();
|
|
130
|
+
const migrations = [
|
|
131
|
+
{ name: '001_clauth_schema', file: 'supabase/migrations/001_clauth_schema.sql' },
|
|
132
|
+
{ name: '002_vault_helpers', file: 'supabase/migrations/002_vault_helpers.sql' },
|
|
133
|
+
];
|
|
134
|
+
for (const m of migrations) {
|
|
135
|
+
const sql = readFileSync(join(ROOT, m.file), 'utf8');
|
|
136
|
+
try {
|
|
137
|
+
await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: sql });
|
|
138
|
+
s4.text = `Migrations: ${m.name} ✓`;
|
|
139
|
+
} catch (e) {
|
|
140
|
+
s4.fail(`Migration ${m.name} failed: ${e.message}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
s4.succeed('Database migrations applied');
|
|
145
|
+
|
|
146
|
+
// Redeploy Edge Function
|
|
147
|
+
const s5 = ora('Redeploying auth-vault Edge Function...').start();
|
|
148
|
+
const fnSource = readFileSync(join(ROOT, 'supabase/functions/auth-vault/index.ts'), 'utf8');
|
|
149
|
+
try {
|
|
150
|
+
const formData = new FormData();
|
|
151
|
+
const metadata = {
|
|
152
|
+
name: 'auth-vault',
|
|
153
|
+
entrypoint_path: 'index.ts',
|
|
154
|
+
verify_jwt: true,
|
|
155
|
+
};
|
|
156
|
+
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
|
157
|
+
formData.append('file', new Blob([fnSource], { type: 'application/typescript' }), 'index.ts');
|
|
158
|
+
|
|
159
|
+
const deployRes = await fetch(
|
|
160
|
+
`${MGMT}/projects/${ref}/functions/deploy?slug=auth-vault`,
|
|
161
|
+
{
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'Authorization': `Bearer ${pat}` },
|
|
164
|
+
body: formData,
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
if (!deployRes.ok) {
|
|
168
|
+
const errText = await deployRes.text().catch(() => deployRes.statusText);
|
|
169
|
+
throw new Error(`HTTP ${deployRes.status}: ${errText}`);
|
|
170
|
+
}
|
|
171
|
+
s5.succeed('auth-vault Edge Function redeployed');
|
|
172
|
+
} catch (e) {
|
|
173
|
+
s5.warn(`Edge Function deploy failed: ${e.message}`);
|
|
174
|
+
console.log(chalk.yellow(' Run manually: supabase functions deploy auth-vault'));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Reinstall Claude skill
|
|
178
|
+
const s9 = ora('Updating Claude skill...').start();
|
|
179
|
+
const skillSrc = join(ROOT, '.clauth-skill');
|
|
180
|
+
if (existsSync(skillSrc)) {
|
|
181
|
+
try {
|
|
182
|
+
const skillDest = join(SKILLS_DIR, 'clauth');
|
|
183
|
+
mkdirSync(skillDest, { recursive: true });
|
|
184
|
+
cpSync(skillSrc, skillDest, { recursive: true, force: true });
|
|
185
|
+
s9.succeed(`Claude skill updated → ${skillDest}`);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
s9.warn(`Skill update skipped: ${e.message}`);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
s9.warn('Skill directory not found — skipping');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log(chalk.green(' Upgrade complete.'));
|
|
195
|
+
console.log(chalk.gray(' Run clauth test to verify.\n'));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// action === 'fresh' — continue with normal install below
|
|
199
|
+
}
|
|
200
|
+
|
|
88
201
|
// ── Step 1: Check internet ────────────────
|
|
89
202
|
const s1 = ora('Checking connectivity...').start();
|
|
90
203
|
try {
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// cli/commands/invite.js
|
|
2
|
+
// clauth invite generate [--uses <n>] [--expires <hours>]
|
|
3
|
+
// clauth invite list
|
|
4
|
+
// clauth invite revoke <code>
|
|
5
|
+
//
|
|
6
|
+
// Admin generates invite codes. Friends redeem them via `clauth join`.
|
|
7
|
+
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
import Conf from "conf";
|
|
12
|
+
import { getConfOptions } from "../conf-path.js";
|
|
13
|
+
import * as api from "../api.js";
|
|
14
|
+
import { getMachineHash, deriveToken } from "../fingerprint.js";
|
|
15
|
+
|
|
16
|
+
// ============================================================
|
|
17
|
+
// Generate a human-friendly invite code: XXXX-XXXX-XXXX
|
|
18
|
+
// No ambiguous characters (0/O, 1/I)
|
|
19
|
+
// ============================================================
|
|
20
|
+
function generateCode() {
|
|
21
|
+
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
22
|
+
const segments = [];
|
|
23
|
+
for (let s = 0; s < 3; s++) {
|
|
24
|
+
let seg = "";
|
|
25
|
+
for (let i = 0; i < 4; i++) {
|
|
26
|
+
seg += chars[crypto.randomInt(chars.length)];
|
|
27
|
+
}
|
|
28
|
+
segments.push(seg);
|
|
29
|
+
}
|
|
30
|
+
return segments.join("-");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================================
|
|
34
|
+
// Main invite handler
|
|
35
|
+
// ============================================================
|
|
36
|
+
export async function runInvite(action, opts) {
|
|
37
|
+
const config = new Conf(getConfOptions());
|
|
38
|
+
|
|
39
|
+
if (action === "generate") {
|
|
40
|
+
// Requires admin auth — prompt for password
|
|
41
|
+
const inquirer = (await import("inquirer")).default;
|
|
42
|
+
const { pw } = await inquirer.prompt([{
|
|
43
|
+
type: "password",
|
|
44
|
+
name: "pw",
|
|
45
|
+
message: "Admin password:",
|
|
46
|
+
mask: "*",
|
|
47
|
+
validate: v => v.length >= 8 || "Password must be at least 8 characters",
|
|
48
|
+
}]);
|
|
49
|
+
|
|
50
|
+
const machineHash = getMachineHash();
|
|
51
|
+
const { token, timestamp } = deriveToken(pw, machineHash);
|
|
52
|
+
|
|
53
|
+
const code = generateCode();
|
|
54
|
+
const maxUses = parseInt(opts.uses, 10) || 1;
|
|
55
|
+
const expiresHours = parseInt(opts.expires, 10) || 168; // 7 days default
|
|
56
|
+
const expiresAt = new Date(Date.now() + expiresHours * 3600000).toISOString();
|
|
57
|
+
|
|
58
|
+
const spinner = ora("Creating invite...").start();
|
|
59
|
+
|
|
60
|
+
// Try to store invite in Supabase via Edge Function
|
|
61
|
+
let storedRemote = false;
|
|
62
|
+
try {
|
|
63
|
+
const supabaseUrl = config.get("supabase_url");
|
|
64
|
+
const anonKey = config.get("supabase_anon_key");
|
|
65
|
+
|
|
66
|
+
if (supabaseUrl && anonKey) {
|
|
67
|
+
const res = await fetch(`${supabaseUrl}/functions/v1/auth-vault/create-invite`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"Authorization": `Bearer ${anonKey}`,
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
machine_hash: machineHash,
|
|
75
|
+
token,
|
|
76
|
+
timestamp,
|
|
77
|
+
password: pw,
|
|
78
|
+
invite_code: code,
|
|
79
|
+
max_uses: maxUses,
|
|
80
|
+
expires_hours: expiresHours,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (res.ok) {
|
|
85
|
+
const body = await res.json();
|
|
86
|
+
if (body.success) storedRemote = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// Edge Function may not support invites yet — fall through to local
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Always store locally as well (source of truth for `invite list`)
|
|
94
|
+
const invites = config.get("invites") || [];
|
|
95
|
+
invites.push({
|
|
96
|
+
code,
|
|
97
|
+
max_uses: maxUses,
|
|
98
|
+
uses: 0,
|
|
99
|
+
expires_at: expiresAt,
|
|
100
|
+
created_at: new Date().toISOString(),
|
|
101
|
+
stored_remote: storedRemote,
|
|
102
|
+
});
|
|
103
|
+
config.set("invites", invites);
|
|
104
|
+
|
|
105
|
+
spinner.succeed(storedRemote ? "Invite created (synced to vault)" : "Invite created (local only)");
|
|
106
|
+
|
|
107
|
+
console.log(chalk.green(`\n Invite code: ${chalk.bold(code)}`));
|
|
108
|
+
console.log(chalk.gray(` Max uses: ${maxUses}`));
|
|
109
|
+
console.log(chalk.gray(` Expires: ${expiresHours}h (${expiresAt})`));
|
|
110
|
+
console.log("");
|
|
111
|
+
console.log(chalk.cyan(" Share this with your friend:"));
|
|
112
|
+
console.log(chalk.white(` npm install -g @lifeaitools/clauth && clauth join ${code}`));
|
|
113
|
+
console.log("");
|
|
114
|
+
|
|
115
|
+
} else if (action === "list") {
|
|
116
|
+
const invites = config.get("invites") || [];
|
|
117
|
+
if (invites.length === 0) {
|
|
118
|
+
console.log(chalk.gray("\n No invites generated.\n"));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(chalk.cyan("\n Invites:\n"));
|
|
123
|
+
console.log(
|
|
124
|
+
chalk.bold(
|
|
125
|
+
" " +
|
|
126
|
+
"CODE".padEnd(16) +
|
|
127
|
+
"STATUS".padEnd(12) +
|
|
128
|
+
"USES".padEnd(10) +
|
|
129
|
+
"EXPIRES"
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
console.log(" " + "-".repeat(60));
|
|
133
|
+
|
|
134
|
+
for (const inv of invites) {
|
|
135
|
+
const now = new Date();
|
|
136
|
+
const expired = new Date(inv.expires_at) < now;
|
|
137
|
+
const exhausted = inv.uses >= inv.max_uses;
|
|
138
|
+
const status = expired
|
|
139
|
+
? chalk.red("expired")
|
|
140
|
+
: exhausted
|
|
141
|
+
? chalk.yellow("used up")
|
|
142
|
+
: chalk.green("active");
|
|
143
|
+
|
|
144
|
+
const uses = `${inv.uses}/${inv.max_uses}`;
|
|
145
|
+
const expStr = new Date(inv.expires_at).toLocaleDateString();
|
|
146
|
+
|
|
147
|
+
console.log(
|
|
148
|
+
` ${inv.code.padEnd(16)}${status.padEnd(12 + (status.length - (expired ? 7 : exhausted ? 7 : 6)))} ${uses.padEnd(10)}${expStr}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
console.log("");
|
|
152
|
+
|
|
153
|
+
} else if (action === "revoke") {
|
|
154
|
+
const code = opts.code;
|
|
155
|
+
if (!code) {
|
|
156
|
+
console.log(chalk.red("\n Usage: clauth invite revoke <code>\n"));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const invites = config.get("invites") || [];
|
|
161
|
+
const idx = invites.findIndex(i => i.code === code.toUpperCase());
|
|
162
|
+
if (idx === -1) {
|
|
163
|
+
console.log(chalk.red(`\n Invite ${code} not found.\n`));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
invites.splice(idx, 1);
|
|
168
|
+
config.set("invites", invites);
|
|
169
|
+
console.log(chalk.green(`\n Invite ${code} revoked.\n`));
|
|
170
|
+
|
|
171
|
+
} else {
|
|
172
|
+
console.log(chalk.yellow(`\n Unknown invite action: ${action}`));
|
|
173
|
+
console.log(chalk.gray(" Usage: clauth invite generate | list | revoke <code>\n"));
|
|
174
|
+
}
|
|
175
|
+
}
|