@lifeaitools/clauth 1.30.2 → 1.30.3

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.
@@ -1,175 +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
- }
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
+ }