@lifeaitools/clauth 0.1.0 → 0.2.1
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 +59 -53
- package/cli/index.js +18 -9
- package/package.json +54 -54
- package/supabase/functions/auth-vault/index.ts +3 -3
- package/supabase/migrations/001_clauth_schema.sql +12 -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);
|
|
@@ -166,16 +157,31 @@ export async function runInstall() {
|
|
|
166
157
|
const s5 = ora('Deploying auth-vault Edge Function...').start();
|
|
167
158
|
const fnSource = readFileSync(join(ROOT, 'supabase/functions/auth-vault/index.ts'), 'utf8');
|
|
168
159
|
try {
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
160
|
+
// Use the /deploy endpoint with multipart/form-data (not the old /functions endpoint)
|
|
161
|
+
const formData = new FormData();
|
|
162
|
+
|
|
163
|
+
const metadata = {
|
|
164
|
+
name: 'auth-vault',
|
|
165
|
+
entrypoint_path: 'index.ts',
|
|
166
|
+
verify_jwt: true,
|
|
167
|
+
};
|
|
168
|
+
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
|
169
|
+
formData.append('file', new Blob([fnSource], { type: 'application/typescript' }), 'index.ts');
|
|
170
|
+
|
|
171
|
+
const deployRes = await fetch(
|
|
172
|
+
`${MGMT}/projects/${ref}/functions/deploy?slug=auth-vault`,
|
|
173
|
+
{
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: { 'Authorization': `Bearer ${pat}` },
|
|
176
|
+
body: formData,
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (!deployRes.ok) {
|
|
181
|
+
const errText = await deployRes.text().catch(() => deployRes.statusText);
|
|
182
|
+
throw new Error(`HTTP ${deployRes.status}: ${errText}`);
|
|
178
183
|
}
|
|
184
|
+
|
|
179
185
|
s5.succeed('auth-vault Edge Function deployed');
|
|
180
186
|
} catch (e) {
|
|
181
187
|
s5.warn(`Edge Function deploy failed: ${e.message}`);
|
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.1
|
|
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.1",
|
|
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" };
|
|
@@ -77,9 +77,17 @@ alter table public.clauth_audit enable row level security;
|
|
|
77
77
|
|
|
78
78
|
-- service_role bypasses RLS — all access from Edge Function only
|
|
79
79
|
-- No direct anon access to any clauth table
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
create policy "
|
|
80
|
+
do $$ begin
|
|
81
|
+
if not exists (select 1 from pg_policies where policyname = 'no_anon_services' and tablename = 'clauth_services') then
|
|
82
|
+
create policy "no_anon_services" on public.clauth_services for all using (false);
|
|
83
|
+
end if;
|
|
84
|
+
if not exists (select 1 from pg_policies where policyname = 'no_anon_machines' and tablename = 'clauth_machines') then
|
|
85
|
+
create policy "no_anon_machines" on public.clauth_machines for all using (false);
|
|
86
|
+
end if;
|
|
87
|
+
if not exists (select 1 from pg_policies where policyname = 'no_anon_audit' and tablename = 'clauth_audit') then
|
|
88
|
+
create policy "no_anon_audit" on public.clauth_audit for all using (false);
|
|
89
|
+
end if;
|
|
90
|
+
end $$;
|
|
83
91
|
|
|
84
92
|
-- ============================================================
|
|
85
93
|
-- Updated_at trigger
|
|
@@ -89,6 +97,7 @@ returns trigger language plpgsql as $$
|
|
|
89
97
|
begin new.updated_at = now(); return new; end;
|
|
90
98
|
$$;
|
|
91
99
|
|
|
100
|
+
drop trigger if exists clauth_services_updated on public.clauth_services;
|
|
92
101
|
create trigger clauth_services_updated
|
|
93
102
|
before update on public.clauth_services
|
|
94
103
|
for each row execute procedure public.clauth_touch_updated();
|