@lifeaitools/clauth 0.1.0 → 0.2.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/README.md +34 -10
- package/cli/commands/install.js +35 -44
- package/cli/index.js +18 -9
- package/package.json +54 -54
- package/supabase/functions/auth-vault/index.ts +3 -3
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @lifeaitools/clauth
|
|
2
2
|
|
|
3
|
-
Hardware-bound credential vault for the LIFEAI stack. Your machine is the second factor. Keys live in Supabase Vault (AES-256).
|
|
3
|
+
Hardware-bound credential vault for the LIFEAI stack. Your machine is the second factor. Keys live in Supabase Vault (AES-256). Nothing sensitive ever touches a config file.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install -g @
|
|
10
|
+
npm install -g @lifeaitools/clauth
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Then provision your Supabase project:
|
|
@@ -17,10 +17,10 @@ clauth install
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
That's it. `clauth install` handles everything:
|
|
20
|
-
- Creates database tables
|
|
21
|
-
- Deploys the Edge Function
|
|
20
|
+
- Creates all database tables
|
|
21
|
+
- Deploys the `auth-vault` Edge Function
|
|
22
22
|
- Generates HMAC salt + bootstrap token
|
|
23
|
-
- Tests the connection
|
|
23
|
+
- Tests the connection end-to-end
|
|
24
24
|
- Installs the Claude skill
|
|
25
25
|
|
|
26
26
|
At the end it prints a **bootstrap token** — save it for the next step.
|
|
@@ -47,7 +47,7 @@ clauth status # → 12 services, all NO KEY
|
|
|
47
47
|
|
|
48
48
|
Two things from Supabase:
|
|
49
49
|
|
|
50
|
-
**1. Project ref** — the last segment of your project URL:
|
|
50
|
+
**1. Project ref** — the last segment of your Supabase project URL:
|
|
51
51
|
`https://supabase.com/dashboard/project/` **`your-ref-here`**
|
|
52
52
|
|
|
53
53
|
**2. Personal Access Token (PAT)**:
|
|
@@ -57,6 +57,16 @@ Two things from Supabase:
|
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
60
|
+
## Writing Your First Key
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
clauth write key github # prompts for value
|
|
64
|
+
clauth enable github
|
|
65
|
+
clauth get github
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
60
70
|
## Command Reference
|
|
61
71
|
|
|
62
72
|
```
|
|
@@ -71,9 +81,9 @@ clauth enable <svc|all> Activate service
|
|
|
71
81
|
clauth disable <svc|all> Suspend service
|
|
72
82
|
clauth get <service> Retrieve a key
|
|
73
83
|
|
|
74
|
-
clauth add service <
|
|
75
|
-
clauth remove service <
|
|
76
|
-
clauth revoke <svc|all> Delete key (destructive
|
|
84
|
+
clauth add service <n> Register new service
|
|
85
|
+
clauth remove service <n> Remove service
|
|
86
|
+
clauth revoke <svc|all> Delete key (destructive)
|
|
77
87
|
```
|
|
78
88
|
|
|
79
89
|
## Built-in Services
|
|
@@ -98,4 +108,18 @@ Nothing stored locally. Password never persisted. Machine hash is one-way only.
|
|
|
98
108
|
|
|
99
109
|
---
|
|
100
110
|
|
|
111
|
+
## Releasing a New Version (maintainers)
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# 1. Bump version in package.json
|
|
115
|
+
# 2. Commit and tag
|
|
116
|
+
git tag v0.1.1
|
|
117
|
+
git push --tags
|
|
118
|
+
# GitHub Actions publishes automatically via Trusted Publishing
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
101
123
|
> Life before Profits. — LIFEAI / PRT
|
|
124
|
+
>
|
|
125
|
+
> ☕ [Support this project](https://github.com/sponsors/DaveLadouceur)
|
package/cli/commands/install.js
CHANGED
|
@@ -33,45 +33,28 @@ const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
|
|
|
33
33
|
: join(process.env.HOME || '', '.claude', 'skills'));
|
|
34
34
|
|
|
35
35
|
// ─────────────────────────────────────────────
|
|
36
|
-
// Prompt helpers
|
|
36
|
+
// Prompt helpers — single shared readline to
|
|
37
|
+
// avoid losing buffered stdin between prompts
|
|
37
38
|
// ─────────────────────────────────────────────
|
|
38
39
|
|
|
40
|
+
let _rl;
|
|
41
|
+
function getRL() {
|
|
42
|
+
if (!_rl) {
|
|
43
|
+
_rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
44
|
+
}
|
|
45
|
+
return _rl;
|
|
46
|
+
}
|
|
47
|
+
function closeRL() {
|
|
48
|
+
if (_rl) { _rl.close(); _rl = null; }
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
function ask(question) {
|
|
40
|
-
|
|
41
|
-
return new Promise(res => rl.question(question, a => { rl.close(); res(a.trim()); }));
|
|
52
|
+
return new Promise(res => getRL().question(question, a => res(a.trim())));
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
function askSecret(label) {
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
});
|
|
56
|
+
// In non-TTY (piped input, CI, Git Bash on Windows), just read a line
|
|
57
|
+
return ask(label);
|
|
75
58
|
}
|
|
76
59
|
|
|
77
60
|
// ─────────────────────────────────────────────
|
|
@@ -89,14 +72,16 @@ async function mgmt(pat, method, path, body) {
|
|
|
89
72
|
throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
|
|
90
73
|
}
|
|
91
74
|
if (res.status === 204) return {};
|
|
92
|
-
|
|
75
|
+
const text = await res.text();
|
|
76
|
+
if (!text) return {};
|
|
77
|
+
return JSON.parse(text);
|
|
93
78
|
}
|
|
94
79
|
|
|
95
80
|
// ─────────────────────────────────────────────
|
|
96
81
|
// Main install command
|
|
97
82
|
// ─────────────────────────────────────────────
|
|
98
83
|
|
|
99
|
-
export async function runInstall() {
|
|
84
|
+
export async function runInstall(opts = {}) {
|
|
100
85
|
console.log(chalk.cyan('\n🔐 clauth install\n'));
|
|
101
86
|
|
|
102
87
|
// ── Step 1: Check internet ────────────────
|
|
@@ -112,16 +97,22 @@ export async function runInstall() {
|
|
|
112
97
|
}
|
|
113
98
|
|
|
114
99
|
// ── Step 2: Collect credentials ───────────
|
|
115
|
-
|
|
116
|
-
|
|
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'));
|
|
100
|
+
let ref = opts.ref;
|
|
101
|
+
let pat = opts.pat;
|
|
122
102
|
|
|
123
|
-
|
|
124
|
-
|
|
103
|
+
if (!ref || !pat) {
|
|
104
|
+
console.log(chalk.cyan('\nYou need two things from Supabase:\n'));
|
|
105
|
+
console.log(chalk.gray(' Project ref: last segment of your project URL'));
|
|
106
|
+
console.log(chalk.gray(' e.g. supabase.com/dashboard/project/') + chalk.white('uvojezuorjgqzmhhgluu'));
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(chalk.gray(' Personal Access Token (PAT):'));
|
|
109
|
+
console.log(chalk.gray(' supabase.com/dashboard/account/tokens → Generate new token'));
|
|
110
|
+
console.log(chalk.gray(' (This is NOT your anon key or service_role key)\n'));
|
|
111
|
+
|
|
112
|
+
if (!ref) ref = await ask(chalk.white('Supabase project ref: '));
|
|
113
|
+
if (!pat) pat = await askSecret(chalk.white('Supabase PAT: '));
|
|
114
|
+
closeRL();
|
|
115
|
+
}
|
|
125
116
|
|
|
126
117
|
if (!ref || !pat) {
|
|
127
118
|
console.log(chalk.red('\n✗ Both required.\n')); process.exit(1);
|
package/cli/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as api from "./api.js";
|
|
|
11
11
|
import os from "os";
|
|
12
12
|
|
|
13
13
|
const config = new Conf({ projectName: "clauth" });
|
|
14
|
-
const VERSION = "0.
|
|
14
|
+
const VERSION = "0.2.0";
|
|
15
15
|
|
|
16
16
|
// ============================================================
|
|
17
17
|
// Password prompt helper
|
|
@@ -55,8 +55,10 @@ import { runInstall } from './commands/install.js';
|
|
|
55
55
|
program
|
|
56
56
|
.command('install')
|
|
57
57
|
.description('Provision Supabase, deploy Edge Function, install Claude skill')
|
|
58
|
-
.
|
|
59
|
-
|
|
58
|
+
.option('--ref <ref>', 'Supabase project ref')
|
|
59
|
+
.option('--pat <pat>', 'Supabase Personal Access Token')
|
|
60
|
+
.action(async (opts) => {
|
|
61
|
+
await runInstall(opts);
|
|
60
62
|
});
|
|
61
63
|
|
|
62
64
|
// ──────────────────────────────────────────────
|
|
@@ -67,6 +69,7 @@ program
|
|
|
67
69
|
.description("Register this machine with the vault (run after clauth install)")
|
|
68
70
|
.option("--admin-token <token>", "Bootstrap token (from clauth install output)")
|
|
69
71
|
.option("--label <label>", "Human label for this machine")
|
|
72
|
+
.option("-p, --pw <password>", "Password (skip interactive prompt)")
|
|
70
73
|
.action(async (opts) => {
|
|
71
74
|
console.log(chalk.cyan("\n🔐 clauth setup\n"));
|
|
72
75
|
|
|
@@ -79,12 +82,18 @@ program
|
|
|
79
82
|
}
|
|
80
83
|
console.log(chalk.gray(` Project: ${savedUrl}\n`));
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
let answers;
|
|
86
|
+
if (opts.pw && opts.adminToken) {
|
|
87
|
+
// Non-interactive mode — all flags provided
|
|
88
|
+
answers = { label: opts.label || os.hostname(), pw: opts.pw, adminTk: opts.adminToken };
|
|
89
|
+
} else {
|
|
90
|
+
answers = await inquirer.prompt([
|
|
91
|
+
{ type: "input", name: "label", message: "Machine label:", default: opts.label || os.hostname() },
|
|
92
|
+
{ type: "password", name: "pw", message: "Set password:", mask: "*", default: opts.pw || "" },
|
|
93
|
+
{ type: "password", name: "adminTk", message: "Bootstrap token:", mask: "*",
|
|
94
|
+
default: opts.adminToken || "" },
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
88
97
|
|
|
89
98
|
const spinner = ora("Registering machine with vault...").start();
|
|
90
99
|
try {
|
package/package.json
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@lifeaitools/clauth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"clauth": "./cli/index.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "bash scripts/build.sh"
|
|
11
|
-
},
|
|
12
|
-
"dependencies": {
|
|
13
|
-
"chalk": "^5.3.0",
|
|
14
|
-
"commander": "^12.1.0",
|
|
15
|
-
"conf": "^13.0.0",
|
|
16
|
-
"inquirer": "^10.1.0",
|
|
17
|
-
"node-fetch": "^3.3.2",
|
|
18
|
-
"ora": "^8.1.0"
|
|
19
|
-
},
|
|
20
|
-
"engines": {
|
|
21
|
-
"node": ">=18.0.0"
|
|
22
|
-
},
|
|
23
|
-
"keywords": [
|
|
24
|
-
"lifeai",
|
|
25
|
-
"credentials",
|
|
26
|
-
"vault",
|
|
27
|
-
"cli",
|
|
28
|
-
"prt"
|
|
29
|
-
],
|
|
30
|
-
"author": "Dave Ladouceur <dave@life.ai>",
|
|
31
|
-
"license": "MIT",
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "https://github.com/LIFEAI/clauth.git"
|
|
35
|
-
},
|
|
36
|
-
"devDependencies": {
|
|
37
|
-
"javascript-obfuscator": "^5.3.0"
|
|
38
|
-
},
|
|
39
|
-
"files": [
|
|
40
|
-
"cli/",
|
|
41
|
-
"scripts/bin/",
|
|
42
|
-
"scripts/bootstrap.cjs",
|
|
43
|
-
"scripts/build.sh",
|
|
44
|
-
"supabase/",
|
|
45
|
-
".clauth-skill/",
|
|
46
|
-
"install.sh",
|
|
47
|
-
"install.ps1",
|
|
48
|
-
"README.md"
|
|
49
|
-
],
|
|
50
|
-
"publishConfig": {
|
|
51
|
-
"access": "public"
|
|
52
|
-
},
|
|
53
|
-
"homepage": "https://github.com/LIFEAI/clauth"
|
|
54
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@lifeaitools/clauth",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clauth": "./cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "bash scripts/build.sh"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"chalk": "^5.3.0",
|
|
14
|
+
"commander": "^12.1.0",
|
|
15
|
+
"conf": "^13.0.0",
|
|
16
|
+
"inquirer": "^10.1.0",
|
|
17
|
+
"node-fetch": "^3.3.2",
|
|
18
|
+
"ora": "^8.1.0"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.0.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"lifeai",
|
|
25
|
+
"credentials",
|
|
26
|
+
"vault",
|
|
27
|
+
"cli",
|
|
28
|
+
"prt"
|
|
29
|
+
],
|
|
30
|
+
"author": "Dave Ladouceur <dave@life.ai>",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/LIFEAI/clauth.git"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"javascript-obfuscator": "^5.3.0"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"cli/",
|
|
41
|
+
"scripts/bin/",
|
|
42
|
+
"scripts/bootstrap.cjs",
|
|
43
|
+
"scripts/build.sh",
|
|
44
|
+
"supabase/",
|
|
45
|
+
".clauth-skill/",
|
|
46
|
+
"install.sh",
|
|
47
|
+
"install.ps1",
|
|
48
|
+
"README.md"
|
|
49
|
+
],
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/LIFEAI/clauth"
|
|
54
|
+
}
|
|
@@ -81,11 +81,11 @@ async function validateHMAC(body: {
|
|
|
81
81
|
return { valid: false, reason: "machine_disabled" };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
// Expected token = HMAC(machine_hash
|
|
84
|
+
// Expected token = HMAC(machine_hash:window, password)
|
|
85
|
+
// Client derives token with password only; server verifies the same way
|
|
85
86
|
const window = Math.floor(body.timestamp / REPLAY_WINDOW_MS);
|
|
86
87
|
const message = `${body.machine_hash}:${window}`;
|
|
87
|
-
const
|
|
88
|
-
const expected = await hmacSha256(hmacKey, message);
|
|
88
|
+
const expected = await hmacSha256(body.password, message);
|
|
89
89
|
|
|
90
90
|
if (expected !== body.token) {
|
|
91
91
|
return { valid: false, reason: "invalid_token" };
|