@lifeaitools/clauth 1.16.9 → 1.18.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 CHANGED
@@ -37,22 +37,109 @@ const VAULT_FETCH_TIMEOUT_MS = (() => {
37
37
  return Number.isFinite(n) && n > 0 ? n : 8000;
38
38
  })();
39
39
 
40
+ // ============================================================
41
+ // Backend / external-resource error model
42
+ // ============================================================
43
+ // A VaultBackendError means the vault backend (the auth-vault Edge Function or
44
+ // the Postgres database behind it) never rendered an auth verdict — the request
45
+ // timed out, the network failed, the function 5xx'd, the DB was unreachable, or
46
+ // we were rate-limited. These are TRANSIENT and must NEVER be counted as an
47
+ // authentication failure (a wrong password). Treating a database timeout as a
48
+ // bad password is the bug that turned a DB blip into a permanent vault lockout.
49
+ export class VaultBackendError extends Error {
50
+ constructor(kind, message, { status = null, detail = null, cause = null } = {}) {
51
+ super(message);
52
+ this.name = "VaultBackendError";
53
+ this.kind = kind; // timeout | network | server_error | rate_limited | db_error | unknown
54
+ this.isBackend = true; // marker for callers: do not strike
55
+ this.retriable = true; // the verdict was never rendered — safe to retry
56
+ this.status = status; // HTTP status if the server responded
57
+ this.detail = detail; // human-readable backend detail
58
+ if (cause) this.cause = cause;
59
+ }
60
+ }
61
+
62
+ // Server `reason` strings that are genuine AUTH VERDICTS (the backend evaluated
63
+ // the credential and answered). These DO count — they are not backend errors.
64
+ const AUTH_VERDICT_REASONS = [
65
+ "invalid_token", "wrong_password", "invalid_password",
66
+ "machine_locked", "machine_disabled", "machine_not_found",
67
+ ];
68
+
69
+ // Classify a server-supplied `reason` string. Returns a VaultBackendError kind
70
+ // when the reason describes a backend/transient condition, or null when the
71
+ // reason is a real auth verdict (or unknown — caller decides).
72
+ export function classifyServerReason(reason) {
73
+ if (!reason || typeof reason !== "string") return null;
74
+ const r = reason.toLowerCase();
75
+ if (AUTH_VERDICT_REASONS.some((v) => r.includes(v))) return null; // genuine verdict
76
+ if (/rate[_\s-]?limit/.test(r)) return "rate_limited";
77
+ if (/(database|\bdb\b|postgres|connection|pool|statement timeout|unavailable|internal|timeout|5\d\d)/.test(r)) return "db_error";
78
+ return null;
79
+ }
80
+
81
+ // Classify a thrown error (transport/timeout/network/typed). Returns a kind
82
+ // string when it is a backend/transient failure, or null when it is not.
83
+ export function classifyBackendError(err) {
84
+ if (!err) return null;
85
+ if (err instanceof VaultBackendError) return err.kind;
86
+ const name = err.name || "";
87
+ const msg = (err.message || "").toString();
88
+ if (name === "TimeoutError" || name === "AbortError" || /\babort(ed)?\b|\btim(e|ed)?\s*out\b|timeout/i.test(msg)) return "timeout";
89
+ if (/fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ECONNRESET|EPIPE|network|socket hang/i.test(msg)) return "network";
90
+ return null;
91
+ }
92
+
40
93
  async function post(route, body) {
41
94
  const url = `${getBaseUrl()}/${route}`;
42
95
  const anonKey = getAnonKey();
43
96
 
44
- const res = await fetch(url, {
45
- method: "POST",
46
- headers: {
47
- "Content-Type": "application/json",
48
- "Authorization": `Bearer ${anonKey}`
49
- },
50
- body: JSON.stringify(body),
51
- signal: AbortSignal.timeout(VAULT_FETCH_TIMEOUT_MS)
52
- });
97
+ let res;
98
+ try {
99
+ res = await fetch(url, {
100
+ method: "POST",
101
+ headers: {
102
+ "Content-Type": "application/json",
103
+ "Authorization": `Bearer ${anonKey}`
104
+ },
105
+ body: JSON.stringify(body),
106
+ signal: AbortSignal.timeout(VAULT_FETCH_TIMEOUT_MS)
107
+ });
108
+ } catch (err) {
109
+ // Transport-level failure — the backend never answered. Surface a typed,
110
+ // non-strike error so callers report "backend unreachable", not "bad password".
111
+ const kind = classifyBackendError(err) || "network";
112
+ const why = kind === "timeout"
113
+ ? `vault backend did not respond within ${VAULT_FETCH_TIMEOUT_MS}ms`
114
+ : `cannot reach vault backend (${err.message || "network error"})`;
115
+ throw new VaultBackendError(kind, why, { detail: err.message || String(err), cause: err });
116
+ }
117
+
118
+ let data;
119
+ try {
120
+ data = await res.json();
121
+ } catch (err) {
122
+ // 2xx/5xx with a non-JSON or empty body — the function errored without a
123
+ // structured verdict. Transient backend condition, not an auth failure.
124
+ throw new VaultBackendError("server_error", `vault backend returned an unreadable response (HTTP ${res.status})`, { status: res.status, detail: err.message });
125
+ }
126
+
127
+ // HTTP 5xx (or any non-ok with no structured error) = backend problem, never a verdict.
128
+ if (res.status >= 500 || (!res.ok && !data.error)) {
129
+ const reasonKind = classifyServerReason(data.reason || data.error) || "server_error";
130
+ throw new VaultBackendError(reasonKind, `vault backend error (HTTP ${res.status})`, { status: res.status, detail: data.error || data.reason || `HTTP ${res.status}` });
131
+ }
132
+
133
+ // 4xx WITH a structured error: could be a real verdict (invalid_token) or a
134
+ // transient backend signal surfaced as 4xx (rate_limited). Promote the latter
135
+ // to a typed backend error; leave genuine verdicts in `data` for the caller.
136
+ if (data.error) {
137
+ const reasonKind = classifyServerReason(data.reason || data.error);
138
+ if (reasonKind) {
139
+ throw new VaultBackendError(reasonKind, `vault backend ${reasonKind.replace("_", " ")}`, { status: res.status, detail: data.error || data.reason });
140
+ }
141
+ }
53
142
 
54
- const data = await res.json();
55
- if (!res.ok && !data.error) throw new Error(`HTTP ${res.status}`);
56
143
  return data;
57
144
  }
58
145
 
@@ -146,5 +233,6 @@ export async function redeemEnrollment(machineHash, seedHash, label, enrollmentC
146
233
 
147
234
  export default {
148
235
  retrieve, write, enable, addService, updateService, removeService, revoke,
149
- status, test, createEnrollment, registerMachine, redeemEnrollment, getBaseUrl, getAnonKey
236
+ status, test, createEnrollment, registerMachine, redeemEnrollment, getBaseUrl, getAnonKey,
237
+ VaultBackendError, classifyBackendError, classifyServerReason
150
238
  };
@@ -1,20 +1,20 @@
1
- @echo off
2
- setlocal
3
- set "CODEVELOP_SESSION=${SESSION_ID}"
4
- set "CODEVELOP_PEER=${PEER}"
5
- set "CODEVELOP_TARGET=${TARGET_PEER}"
6
- set "CODEVELOP_BASE_URL=${BASE_URL}"
7
- set "CODEVELOP_MANIFEST=${MANIFEST}"
8
- set "CODEVELOP_CONTEXT=${CONTEXT_FILE}"
9
- set "CELL_ROLE=codevelop-${PEER}"
10
- cd /d "${CWD}"
11
- if /i "%~1"=="--print-only" (
12
- echo command: ${PRINT_COMMAND}
13
- exit /b 0
14
- )
15
- echo Co-develop ${PEER} ready. Session ${SESSION_ID}. Listening for collaboration.
16
- echo Context: ${CONTEXT_FILE}
17
- node "${CLAUTH_CLI}" codevelop check-partner --repo "${REPO}" --peer ${PEER}
18
- ${COMMAND}
19
- set EXITCODE=%ERRORLEVEL%
20
- endlocal & exit /b %EXITCODE%
1
+ @echo off
2
+ setlocal
3
+ set "CODEVELOP_SESSION=${SESSION_ID}"
4
+ set "CODEVELOP_PEER=${PEER}"
5
+ set "CODEVELOP_TARGET=${TARGET_PEER}"
6
+ set "CODEVELOP_BASE_URL=${BASE_URL}"
7
+ set "CODEVELOP_MANIFEST=${MANIFEST}"
8
+ set "CODEVELOP_CONTEXT=${CONTEXT_FILE}"
9
+ set "CELL_ROLE=codevelop-${PEER}"
10
+ cd /d "${CWD}"
11
+ if /i "%~1"=="--print-only" (
12
+ echo command: ${PRINT_COMMAND}
13
+ exit /b 0
14
+ )
15
+ echo Co-develop ${PEER} ready. Session ${SESSION_ID}. Listening for collaboration.
16
+ echo Context: ${CONTEXT_FILE}
17
+ node "${CLAUTH_CLI}" codevelop check-partner --repo "${REPO}" --peer ${PEER}
18
+ ${COMMAND}
19
+ set EXITCODE=%ERRORLEVEL%
20
+ endlocal & exit /b %EXITCODE%
@@ -1,7 +1,7 @@
1
- @echo off
2
- if not exist "${ACTIVE_LAUNCHER}" (
3
- echo No active co-develop launcher has been written.
4
- echo Run: clauth codevelop start --repo "${REPO}" --port ${PORT} --start-isolated-clauth
5
- exit /b 1
6
- )
7
- call "${ACTIVE_LAUNCHER}" %*
1
+ @echo off
2
+ if not exist "${ACTIVE_LAUNCHER}" (
3
+ echo No active co-develop launcher has been written.
4
+ echo Run: clauth codevelop start --repo "${REPO}" --port ${PORT} --start-isolated-clauth
5
+ exit /b 1
6
+ )
7
+ call "${ACTIVE_LAUNCHER}" %*
@@ -1,48 +1,48 @@
1
- {
2
- "profiles": [
3
- {
4
- "name": "Co Develop Claude",
5
- "guid": "{a2f8548f-82e6-44b4-bdbb-d934e8881f01}",
6
- "commandline": "cmd.exe /k \"${CLAUTH_APPDATA}\\codevelop-claude.cmd\"",
7
- "startingDirectory": "${REPO}",
8
- "environment": {
9
- "CELL_ROLE": "codevelop-claude"
10
- },
11
- "icon": "🔴",
12
- "tabColor": "#B91C1C",
13
- "colorScheme": "Dimidium"
14
- },
15
- {
16
- "name": "Co Develop Codex",
17
- "guid": "{31a52b07-6efd-4b3d-96dd-ded73488140a}",
18
- "commandline": "cmd.exe /k \"${CLAUTH_APPDATA}\\codevelop-codex.cmd\"",
19
- "startingDirectory": "${REPO}",
20
- "environment": {
21
- "CELL_ROLE": "codevelop-codex"
22
- },
23
- "icon": "🧬",
24
- "tabColor": "#0F766E",
25
- "colorScheme": "Dimidium"
26
- }
27
- ],
28
- "layout": {
29
- "wtArgs": [
30
- "-w",
31
- "new",
32
- "new-tab",
33
- "--profile",
34
- "Co Develop Claude",
35
- "--title",
36
- "Co Develop Claude",
37
- ";",
38
- "split-pane",
39
- "-V",
40
- "--size",
41
- "0.50",
42
- "--profile",
43
- "Co Develop Codex",
44
- "--title",
45
- "Co Develop Codex"
46
- ]
47
- }
48
- }
1
+ {
2
+ "profiles": [
3
+ {
4
+ "name": "Co Develop Claude",
5
+ "guid": "{a2f8548f-82e6-44b4-bdbb-d934e8881f01}",
6
+ "commandline": "cmd.exe /k \"${CLAUTH_APPDATA}\\codevelop-claude.cmd\"",
7
+ "startingDirectory": "${REPO}",
8
+ "environment": {
9
+ "CELL_ROLE": "codevelop-claude"
10
+ },
11
+ "icon": "🔴",
12
+ "tabColor": "#B91C1C",
13
+ "colorScheme": "Dimidium"
14
+ },
15
+ {
16
+ "name": "Co Develop Codex",
17
+ "guid": "{31a52b07-6efd-4b3d-96dd-ded73488140a}",
18
+ "commandline": "cmd.exe /k \"${CLAUTH_APPDATA}\\codevelop-codex.cmd\"",
19
+ "startingDirectory": "${REPO}",
20
+ "environment": {
21
+ "CELL_ROLE": "codevelop-codex"
22
+ },
23
+ "icon": "🧬",
24
+ "tabColor": "#0F766E",
25
+ "colorScheme": "Dimidium"
26
+ }
27
+ ],
28
+ "layout": {
29
+ "wtArgs": [
30
+ "-w",
31
+ "new",
32
+ "new-tab",
33
+ "--profile",
34
+ "Co Develop Claude",
35
+ "--title",
36
+ "Co Develop Claude",
37
+ ";",
38
+ "split-pane",
39
+ "-V",
40
+ "--size",
41
+ "0.50",
42
+ "--profile",
43
+ "Co Develop Codex",
44
+ "--title",
45
+ "Co Develop Codex"
46
+ ]
47
+ }
48
+ }
@@ -1,42 +1,42 @@
1
- # clauth-watchdog.ps1
2
- # Monitors clauth daemon on port 52437. Restarts it if not responding.
3
- # Installed by: npm install -g @lifeaitools/clauth
4
- # Managed by: clauth watchdog [install|uninstall|status]
5
-
6
- $clauth = "$env:APPDATA\npm\clauth.cmd"
7
- $logFile = "$env:APPDATA\clauth\watchdog.log"
8
- $pingUrl = "http://127.0.0.1:52437/ping"
9
- $interval = 15
10
-
11
- function Write-Log($msg) {
12
- $ts = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
13
- $line = "[$ts] $msg"
14
- Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
15
- }
16
-
17
- Write-Log "Watchdog started (PID $PID, clauth=$clauth)"
18
-
19
- while ($true) {
20
- $alive = $false
21
- try {
22
- $r = Invoke-WebRequest -Uri $pingUrl -TimeoutSec 3 -UseBasicParsing -ErrorAction Stop
23
- if ($r.StatusCode -eq 200) { $alive = $true }
24
- } catch { }
25
-
26
- if (-not $alive) {
27
- Write-Log "Daemon not responding — restarting..."
28
- Start-Process -FilePath "cmd.exe" `
29
- -ArgumentList "/c `"$clauth`" serve start" `
30
- -WindowStyle Hidden
31
- Start-Sleep -Seconds 5
32
- try {
33
- $r = Invoke-WebRequest -Uri $pingUrl -TimeoutSec 5 -UseBasicParsing -ErrorAction Stop
34
- if ($r.StatusCode -eq 200) { Write-Log "Daemon restarted OK." }
35
- else { Write-Log "WARNING: Daemon still not responding." }
36
- } catch {
37
- Write-Log "WARNING: Daemon still not responding after restart."
38
- }
39
- }
40
-
41
- Start-Sleep -Seconds $interval
42
- }
1
+ # clauth-watchdog.ps1
2
+ # Monitors clauth daemon on port 52437. Restarts it if not responding.
3
+ # Installed by: npm install -g @lifeaitools/clauth
4
+ # Managed by: clauth watchdog [install|uninstall|status]
5
+
6
+ $clauth = "$env:APPDATA\npm\clauth.cmd"
7
+ $logFile = "$env:APPDATA\clauth\watchdog.log"
8
+ $pingUrl = "http://127.0.0.1:52437/ping"
9
+ $interval = 15
10
+
11
+ function Write-Log($msg) {
12
+ $ts = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
13
+ $line = "[$ts] $msg"
14
+ Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
15
+ }
16
+
17
+ Write-Log "Watchdog started (PID $PID, clauth=$clauth)"
18
+
19
+ while ($true) {
20
+ $alive = $false
21
+ try {
22
+ $r = Invoke-WebRequest -Uri $pingUrl -TimeoutSec 3 -UseBasicParsing -ErrorAction Stop
23
+ if ($r.StatusCode -eq 200) { $alive = $true }
24
+ } catch { }
25
+
26
+ if (-not $alive) {
27
+ Write-Log "Daemon not responding — restarting..."
28
+ Start-Process -FilePath "cmd.exe" `
29
+ -ArgumentList "/c `"$clauth`" serve start" `
30
+ -WindowStyle Hidden
31
+ Start-Sleep -Seconds 5
32
+ try {
33
+ $r = Invoke-WebRequest -Uri $pingUrl -TimeoutSec 5 -UseBasicParsing -ErrorAction Stop
34
+ if ($r.StatusCode -eq 200) { Write-Log "Daemon restarted OK." }
35
+ else { Write-Log "WARNING: Daemon still not responding." }
36
+ } catch {
37
+ Write-Log "WARNING: Daemon still not responding after restart."
38
+ }
39
+ }
40
+
41
+ Start-Sleep -Seconds $interval
42
+ }