claude-flow 3.32.1 → 3.32.8
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/.claude/helpers/statusline.cjs +24 -17
- package/.claude-plugin/scripts/ruflo-hook.cjs +2 -2
- package/.claude-plugin/scripts/ruflo-hook.sh +17 -2
- package/package.json +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/auth/client.d.ts +89 -0
- package/v3/@claude-flow/cli/dist/src/auth/client.js +242 -0
- package/v3/@claude-flow/cli/dist/src/auth/constants.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/auth/constants.js +7 -0
- package/v3/@claude-flow/cli/dist/src/auth/scopes.d.ts +14 -0
- package/v3/@claude-flow/cli/dist/src/auth/scopes.js +21 -0
- package/v3/@claude-flow/cli/dist/src/auth/security-bridge.d.ts +36 -0
- package/v3/@claude-flow/cli/dist/src/auth/security-bridge.js +42 -0
- package/v3/@claude-flow/cli/dist/src/auth/session.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/auth/session.js +32 -0
- package/v3/@claude-flow/cli/dist/src/auth/state.d.ts +19 -0
- package/v3/@claude-flow/cli/dist/src/auth/state.js +53 -0
- package/v3/@claude-flow/cli/dist/src/auth/types.d.ts +27 -0
- package/v3/@claude-flow/cli/dist/src/auth/types.js +11 -0
- package/v3/@claude-flow/cli/dist/src/commands/auth.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/commands/auth.js +244 -0
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +211 -4
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +20 -9
- package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.js +277 -0
- package/v3/@claude-flow/cli/dist/src/commands/proxy.js +97 -4
- package/v3/@claude-flow/cli/dist/src/commands/security.js +16 -11
- package/v3/@claude-flow/cli/dist/src/funnel/insights.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/funnel/insights.js +4 -4
- package/v3/@claude-flow/cli/dist/src/funnel/local-signals.d.ts +6 -1
- package/v3/@claude-flow/cli/dist/src/funnel/local-signals.js +29 -20
- package/v3/@claude-flow/cli/dist/src/init/executor.js +2 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +13 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +11 -3
- package/v3/@claude-flow/cli/dist/src/proxy/install.d.ts +29 -0
- package/v3/@claude-flow/cli/dist/src/proxy/install.js +135 -0
- package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.d.ts +61 -0
- package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.js +249 -0
- package/v3/@claude-flow/cli/dist/src/proxy/paths.d.ts +34 -0
- package/v3/@claude-flow/cli/dist/src/proxy/paths.js +70 -0
- package/v3/@claude-flow/cli/dist/src/proxy/release.d.ts +47 -0
- package/v3/@claude-flow/cli/dist/src/proxy/release.js +138 -0
- package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.d.ts +5 -0
- package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.js +61 -0
- package/v3/@claude-flow/cli/dist/src/proxy/verify.d.ts +44 -0
- package/v3/@claude-flow/cli/dist/src/proxy/verify.js +68 -0
- package/v3/@claude-flow/cli/dist/src/security/builtin-aidefence.d.ts +34 -0
- package/v3/@claude-flow/cli/dist/src/security/builtin-aidefence.js +86 -0
- package/v3/@claude-flow/cli/dist/src/services/fable-harness.d.ts +1 -0
- package/v3/@claude-flow/cli/package.json +2 -2
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meta-proxy process lifecycle (ADR-307) — start/stop/status/logs.
|
|
3
|
+
*
|
|
4
|
+
* Adapts daemon.ts's proven pattern (PID file, O_EXCL lockfile for atomic
|
|
5
|
+
* check-then-start, signal-0 liveness, SIGTERM->1000ms->SIGKILL) to a
|
|
6
|
+
* native binary instead of a forked Node process. The binary itself takes
|
|
7
|
+
* no CLI flags — confirmed empirically (2026-07-16): `meta-proxy.exe` has
|
|
8
|
+
* no `--version`/`--help`, and any invocation just starts the server reading
|
|
9
|
+
* its own config file — so `spawn()` here passes zero arguments, always.
|
|
10
|
+
*
|
|
11
|
+
* Foreground `start` (the ADR-307 default) uses `stdio: 'inherit'` and
|
|
12
|
+
* blocks directly — simplest and safest, no log-file redirection needed.
|
|
13
|
+
* `start --service` needs REAL file-descriptor redirection
|
|
14
|
+
* (`stdio: ['ignore', fd, fd]`) + `detached: true` + `unref()` via
|
|
15
|
+
* `child_process.spawn()` directly — `SafeExecutor.executeStreaming()`
|
|
16
|
+
* buffers output in-process, which is wrong for a process meant to outlive
|
|
17
|
+
* the `ruflo` invocation that started it.
|
|
18
|
+
*
|
|
19
|
+
* @module proxy/lifecycle
|
|
20
|
+
*/
|
|
21
|
+
import { spawn } from 'node:child_process';
|
|
22
|
+
import * as fs from 'node:fs';
|
|
23
|
+
import { proxyBinaryPath, proxyPidFilePath, proxyLockFilePath, proxyLogFilePath } from './paths.js';
|
|
24
|
+
export class ProxyNotInstalledError extends Error {
|
|
25
|
+
constructor() {
|
|
26
|
+
super('meta-proxy is not installed. Run: ruflo proxy install');
|
|
27
|
+
this.name = 'ProxyNotInstalledError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class ProxyAlreadyRunningError extends Error {
|
|
31
|
+
pid;
|
|
32
|
+
constructor(pid) {
|
|
33
|
+
super(`meta-proxy is already running (pid ${pid}). Stop it first with: ruflo proxy stop`);
|
|
34
|
+
this.pid = pid;
|
|
35
|
+
this.name = 'ProxyAlreadyRunningError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function requireBinary() {
|
|
39
|
+
const bin = proxyBinaryPath();
|
|
40
|
+
if (!fs.existsSync(bin))
|
|
41
|
+
throw new ProxyNotInstalledError();
|
|
42
|
+
return bin;
|
|
43
|
+
}
|
|
44
|
+
/** Signal-0 liveness probe — cross-platform in Node, throws if the process is dead. */
|
|
45
|
+
function isProcessRunning(pid) {
|
|
46
|
+
try {
|
|
47
|
+
process.kill(pid, 0);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function getProxyStatus() {
|
|
55
|
+
const installed = fs.existsSync(proxyBinaryPath());
|
|
56
|
+
const pidPath = proxyPidFilePath();
|
|
57
|
+
if (!fs.existsSync(pidPath)) {
|
|
58
|
+
return { installed, running: false, pid: null, stalePidFile: false };
|
|
59
|
+
}
|
|
60
|
+
const raw = fs.readFileSync(pidPath, 'utf-8').trim();
|
|
61
|
+
const pid = parseInt(raw, 10);
|
|
62
|
+
if (!Number.isFinite(pid)) {
|
|
63
|
+
return { installed, running: false, pid: null, stalePidFile: true };
|
|
64
|
+
}
|
|
65
|
+
const running = isProcessRunning(pid);
|
|
66
|
+
return { installed, running, pid: running ? pid : null, stalePidFile: !running };
|
|
67
|
+
}
|
|
68
|
+
function writePidFile(pid) {
|
|
69
|
+
fs.writeFileSync(proxyPidFilePath(), String(pid), 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
function clearStalePidFile() {
|
|
72
|
+
try {
|
|
73
|
+
fs.unlinkSync(proxyPidFilePath());
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// already absent
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Atomic check-then-start via an O_EXCL lockfile — the same fix daemon.ts
|
|
81
|
+
* applies for the identical race (#2407/#2484: without this, N concurrent
|
|
82
|
+
* `start` calls all see "not running" and all spawn their own process).
|
|
83
|
+
* Returns the lock fd to release once the PID file has been written (or on
|
|
84
|
+
* any early-return/throw), never before.
|
|
85
|
+
*/
|
|
86
|
+
function acquireStartLock() {
|
|
87
|
+
const lockFile = proxyLockFilePath();
|
|
88
|
+
try {
|
|
89
|
+
return fs.openSync(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
if (e.code === 'EEXIST')
|
|
93
|
+
return null; // another start is mid-spawn
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function releaseStartLock(fd) {
|
|
98
|
+
if (fd === null)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
fs.closeSync(fd);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* ignore */
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
fs.unlinkSync(proxyLockFilePath());
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* ignore */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Foreground start (ADR-307 default) — blocks the caller until the process
|
|
115
|
+
* exits or is interrupted. `stdio: 'inherit'` passes the proxy's own output
|
|
116
|
+
* straight through to the terminal; signals (Ctrl+C) propagate naturally to
|
|
117
|
+
* the child, no manual forwarding needed.
|
|
118
|
+
*/
|
|
119
|
+
export async function startForeground(supervised = false) {
|
|
120
|
+
const bin = requireBinary();
|
|
121
|
+
const status = getProxyStatus();
|
|
122
|
+
// In service mode startBackground has already written this supervisor's
|
|
123
|
+
// PID. Treating it as a competing proxy makes the supervisor immediately
|
|
124
|
+
// exit before it can spawn meta-proxy.
|
|
125
|
+
if (!supervised && status.running && status.pid)
|
|
126
|
+
throw new ProxyAlreadyRunningError(status.pid);
|
|
127
|
+
if (status.stalePidFile)
|
|
128
|
+
clearStalePidFile();
|
|
129
|
+
const child = spawn(bin, [], { stdio: 'inherit', windowsHide: false });
|
|
130
|
+
if (!supervised && child.pid)
|
|
131
|
+
writePidFile(child.pid);
|
|
132
|
+
const cleanup = () => clearStalePidFile();
|
|
133
|
+
process.on('exit', cleanup);
|
|
134
|
+
if (supervised) {
|
|
135
|
+
const forwardSignal = (signal) => {
|
|
136
|
+
if (!child.killed)
|
|
137
|
+
child.kill(signal);
|
|
138
|
+
};
|
|
139
|
+
process.once('SIGTERM', () => forwardSignal('SIGTERM'));
|
|
140
|
+
process.once('SIGINT', () => forwardSignal('SIGINT'));
|
|
141
|
+
}
|
|
142
|
+
await new Promise((resolve) => {
|
|
143
|
+
child.on('exit', () => {
|
|
144
|
+
cleanup();
|
|
145
|
+
resolve();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Background/`--service` start — detaches so the process outlives this
|
|
152
|
+
* `ruflo` invocation, redirecting stdout/stderr to a real log file (not
|
|
153
|
+
* buffered in-process). Returns once the child's PID is confirmed written,
|
|
154
|
+
* without waiting for the process to exit.
|
|
155
|
+
*/
|
|
156
|
+
export async function startBackground() {
|
|
157
|
+
requireBinary();
|
|
158
|
+
const status = getProxyStatus();
|
|
159
|
+
if (status.running && status.pid)
|
|
160
|
+
throw new ProxyAlreadyRunningError(status.pid);
|
|
161
|
+
const lockFd = acquireStartLock();
|
|
162
|
+
try {
|
|
163
|
+
// Re-check under the lock — the dedup above raced a concurrent starter.
|
|
164
|
+
const rechecked = getProxyStatus();
|
|
165
|
+
if (rechecked.running && rechecked.pid)
|
|
166
|
+
throw new ProxyAlreadyRunningError(rechecked.pid);
|
|
167
|
+
if (rechecked.stalePidFile)
|
|
168
|
+
clearStalePidFile();
|
|
169
|
+
const logFd = fs.openSync(proxyLogFilePath(), 'a');
|
|
170
|
+
const cliEntry = process.argv[1];
|
|
171
|
+
if (!cliEntry)
|
|
172
|
+
throw new Error('cannot locate the ruflo CLI entrypoint for supervised service mode');
|
|
173
|
+
const child = spawn(process.execPath, [cliEntry, 'proxy', 'supervise'], {
|
|
174
|
+
stdio: ['ignore', logFd, logFd],
|
|
175
|
+
detached: true,
|
|
176
|
+
windowsHide: true,
|
|
177
|
+
});
|
|
178
|
+
fs.closeSync(logFd); // the child holds its own duplicated fd; safe to close ours
|
|
179
|
+
if (!child.pid)
|
|
180
|
+
throw new Error('ruflo proxy supervisor failed to spawn — no PID returned');
|
|
181
|
+
writePidFile(child.pid);
|
|
182
|
+
child.unref();
|
|
183
|
+
return { pid: child.pid };
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
releaseStartLock(lockFd);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** SIGTERM -> 1000ms -> SIGKILL if still alive, mirroring daemon.ts's killBackgroundDaemon. */
|
|
190
|
+
export async function stopProxy() {
|
|
191
|
+
const status = getProxyStatus();
|
|
192
|
+
if (!status.running || !status.pid) {
|
|
193
|
+
if (status.stalePidFile)
|
|
194
|
+
clearStalePidFile();
|
|
195
|
+
return { wasRunning: false, pid: null };
|
|
196
|
+
}
|
|
197
|
+
const pid = status.pid;
|
|
198
|
+
process.kill(pid, 'SIGTERM');
|
|
199
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
200
|
+
if (isProcessRunning(pid)) {
|
|
201
|
+
process.kill(pid, 'SIGKILL');
|
|
202
|
+
}
|
|
203
|
+
clearStalePidFile();
|
|
204
|
+
return { wasRunning: true, pid };
|
|
205
|
+
}
|
|
206
|
+
const TAIL_BYTES = 64 * 1024; // bounded read — a long-running proxy's log grows over weeks
|
|
207
|
+
export function readProxyLogTail(maxBytes = TAIL_BYTES) {
|
|
208
|
+
const logPath = proxyLogFilePath();
|
|
209
|
+
if (!fs.existsSync(logPath))
|
|
210
|
+
return '';
|
|
211
|
+
const { size } = fs.statSync(logPath);
|
|
212
|
+
const start = Math.max(0, size - maxBytes);
|
|
213
|
+
const fd = fs.openSync(logPath, 'r');
|
|
214
|
+
try {
|
|
215
|
+
const buf = Buffer.alloc(size - start);
|
|
216
|
+
fs.readSync(fd, buf, 0, buf.length, start);
|
|
217
|
+
return buf.toString('utf-8');
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
fs.closeSync(fd);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** Streams new log lines as they're appended, starting from the current end of file. */
|
|
224
|
+
export function watchProxyLog(onLine) {
|
|
225
|
+
const logPath = proxyLogFilePath();
|
|
226
|
+
if (!fs.existsSync(logPath)) {
|
|
227
|
+
throw new Error('no log file yet — meta-proxy has never been started in --service mode');
|
|
228
|
+
}
|
|
229
|
+
let offset = fs.statSync(logPath).size;
|
|
230
|
+
return fs.watch(logPath, () => {
|
|
231
|
+
const { size } = fs.statSync(logPath);
|
|
232
|
+
if (size <= offset)
|
|
233
|
+
return;
|
|
234
|
+
const fd = fs.openSync(logPath, 'r');
|
|
235
|
+
try {
|
|
236
|
+
const buf = Buffer.alloc(size - offset);
|
|
237
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
238
|
+
offset = size;
|
|
239
|
+
for (const line of buf.toString('utf-8').split('\n')) {
|
|
240
|
+
if (line.length > 0)
|
|
241
|
+
onLine(line);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
fs.closeSync(fd);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem layout for the managed meta-proxy binary (ADR-307), all under
|
|
3
|
+
* the same `~/.ruflo` state dir every other funnel/proxy/auth file uses
|
|
4
|
+
* (src/funnel/state.ts's `funnelStateDir()` — respects RUFLO_STATE_DIR for
|
|
5
|
+
* tests). Centralized here so doctor.ts, install.ts, and lifecycle.ts share
|
|
6
|
+
* one definition instead of three copies drifting apart.
|
|
7
|
+
*/
|
|
8
|
+
export declare function proxyBinaryPath(): string;
|
|
9
|
+
export declare function proxyPidFilePath(): string;
|
|
10
|
+
export declare function proxyLockFilePath(): string;
|
|
11
|
+
export declare function proxyLogFilePath(): string;
|
|
12
|
+
export declare function proxyInstallManifestPath(): string;
|
|
13
|
+
export declare function proxyConfigPath(): string;
|
|
14
|
+
export declare function proxyTokenPath(): string;
|
|
15
|
+
export declare function proxyInjectedTokenPath(): string;
|
|
16
|
+
export interface InstallManifest {
|
|
17
|
+
version: string;
|
|
18
|
+
sha256: string;
|
|
19
|
+
verifiedAt: string;
|
|
20
|
+
pubkeyFingerprint: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* True if `bind` (a `host:port` string from proxy-config.toml) targets a
|
|
24
|
+
* loopback address — decides whether the non-loopback exposure warning
|
|
25
|
+
* fires (ADR-307). Recognizes the whole IPv4 127.0.0.0/8 block, IPv6 `::1`,
|
|
26
|
+
* IPv4-mapped IPv6 loopback, and the `localhost` hostname.
|
|
27
|
+
*
|
|
28
|
+
* A plain `bind.startsWith('127.0.0.1')` check (the obvious first draft)
|
|
29
|
+
* misclassifies `[::1]:port` as non-loopback — a real bug meta-proxy's own
|
|
30
|
+
* Rust `is_loopback_bind` fixed for the identical reason; this mirrors that
|
|
31
|
+
* fix rather than repeating the mistake on the TypeScript side.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isLoopbackBind(bind: string): boolean;
|
|
34
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem layout for the managed meta-proxy binary (ADR-307), all under
|
|
3
|
+
* the same `~/.ruflo` state dir every other funnel/proxy/auth file uses
|
|
4
|
+
* (src/funnel/state.ts's `funnelStateDir()` — respects RUFLO_STATE_DIR for
|
|
5
|
+
* tests). Centralized here so doctor.ts, install.ts, and lifecycle.ts share
|
|
6
|
+
* one definition instead of three copies drifting apart.
|
|
7
|
+
*/
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { funnelStateDir } from '../funnel/index.js';
|
|
10
|
+
const BINARY_NAME = process.platform === 'win32' ? 'meta-proxy.exe' : 'meta-proxy';
|
|
11
|
+
export function proxyBinaryPath() {
|
|
12
|
+
return join(funnelStateDir(), 'bin', BINARY_NAME);
|
|
13
|
+
}
|
|
14
|
+
export function proxyPidFilePath() {
|
|
15
|
+
return join(funnelStateDir(), 'proxy.pid');
|
|
16
|
+
}
|
|
17
|
+
export function proxyLockFilePath() {
|
|
18
|
+
return join(funnelStateDir(), 'proxy.lock');
|
|
19
|
+
}
|
|
20
|
+
export function proxyLogFilePath() {
|
|
21
|
+
return join(funnelStateDir(), 'proxy.log');
|
|
22
|
+
}
|
|
23
|
+
export function proxyInstallManifestPath() {
|
|
24
|
+
return join(funnelStateDir(), 'proxy', 'install-manifest.json');
|
|
25
|
+
}
|
|
26
|
+
export function proxyConfigPath() {
|
|
27
|
+
return join(funnelStateDir(), 'proxy-config.toml');
|
|
28
|
+
}
|
|
29
|
+
export function proxyTokenPath() {
|
|
30
|
+
return join(funnelStateDir(), 'proxy-token');
|
|
31
|
+
}
|
|
32
|
+
export function proxyInjectedTokenPath() {
|
|
33
|
+
return join(funnelStateDir(), 'proxy-injected-token.json');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* True if `bind` (a `host:port` string from proxy-config.toml) targets a
|
|
37
|
+
* loopback address — decides whether the non-loopback exposure warning
|
|
38
|
+
* fires (ADR-307). Recognizes the whole IPv4 127.0.0.0/8 block, IPv6 `::1`,
|
|
39
|
+
* IPv4-mapped IPv6 loopback, and the `localhost` hostname.
|
|
40
|
+
*
|
|
41
|
+
* A plain `bind.startsWith('127.0.0.1')` check (the obvious first draft)
|
|
42
|
+
* misclassifies `[::1]:port` as non-loopback — a real bug meta-proxy's own
|
|
43
|
+
* Rust `is_loopback_bind` fixed for the identical reason; this mirrors that
|
|
44
|
+
* fix rather than repeating the mistake on the TypeScript side.
|
|
45
|
+
*/
|
|
46
|
+
export function isLoopbackBind(bind) {
|
|
47
|
+
const hostPart = (host) => {
|
|
48
|
+
const stripped = host.replace(/^\[/, '').replace(/\]$/, '');
|
|
49
|
+
if (stripped.toLowerCase() === 'localhost')
|
|
50
|
+
return true;
|
|
51
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(stripped))
|
|
52
|
+
return true; // 127.0.0.0/8
|
|
53
|
+
if (stripped === '::1')
|
|
54
|
+
return true;
|
|
55
|
+
if (/^::ffff:127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/i.test(stripped))
|
|
56
|
+
return true; // IPv4-mapped loopback
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
// IPv6 bracketed form: [::1]:11435
|
|
60
|
+
const bracketed = bind.match(/^\[([^\]]+)\]:\d+$/);
|
|
61
|
+
if (bracketed)
|
|
62
|
+
return hostPart(bracketed[1]);
|
|
63
|
+
// host:port (IPv4 or hostname) — split on the LAST colon so a bare IPv6
|
|
64
|
+
// address without brackets (ambiguous, shouldn't occur in practice) isn't
|
|
65
|
+
// misparsed as host:port.
|
|
66
|
+
const lastColon = bind.lastIndexOf(':');
|
|
67
|
+
const host = lastColon === -1 ? bind : bind.slice(0, lastColon);
|
|
68
|
+
return hostPart(host);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meta-proxy release resolution + download (ADR-307).
|
|
3
|
+
*
|
|
4
|
+
* Two download paths, deliberately not conflated:
|
|
5
|
+
* - **Production** (`downloadReleaseAsset`): public, signed assets from
|
|
6
|
+
* cognitum-one/meta-proxy-dist. Source remains private; normal users need
|
|
7
|
+
* neither GitHub authentication nor access to the source repository.
|
|
8
|
+
* - **Dev-only** (`downloadViaGhCli`): shells out to `gh release download`,
|
|
9
|
+
* gated behind `RUFLO_DEV_PROXY_INSTALL=1` so it is never reachable by
|
|
10
|
+
* accident, and always logs loudly that it is a developer path.
|
|
11
|
+
*
|
|
12
|
+
* @module proxy/release
|
|
13
|
+
*/
|
|
14
|
+
export declare const TARGET_TRIPLES: readonly ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "x86_64-pc-windows-msvc"];
|
|
15
|
+
export type TargetTriple = (typeof TARGET_TRIPLES)[number];
|
|
16
|
+
export declare class UnsupportedPlatformError extends Error {
|
|
17
|
+
constructor(platform: string, arch: string);
|
|
18
|
+
}
|
|
19
|
+
/** Maps the running Node process's platform/arch onto one of meta-proxy's 5 published triples. */
|
|
20
|
+
export declare function detectTargetTriple(platform?: string, arch?: string): TargetTriple;
|
|
21
|
+
export declare function releaseArchiveExtension(triple: TargetTriple): 'zip' | 'tar.gz';
|
|
22
|
+
export declare function releaseAssetFilename(version: string, triple: TargetTriple): string;
|
|
23
|
+
export interface ReleaseAssets {
|
|
24
|
+
archiveBytes: Buffer;
|
|
25
|
+
archiveFilename: string;
|
|
26
|
+
sumsBytes: Buffer;
|
|
27
|
+
sigBase64: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Dev-only fallback: `gh release download` via SafeExecutor into `destDir`.
|
|
31
|
+
* Requires the caller's environment to already have `gh` authenticated
|
|
32
|
+
* against a GitHub account with access to the private meta-proxy repo — this
|
|
33
|
+
* is NOT something a normal ruflo end user has, which is exactly why this
|
|
34
|
+
* path is gated and logged, not the default.
|
|
35
|
+
*/
|
|
36
|
+
export declare function downloadViaGhCli(destDir: string, version: string, triple: TargetTriple, log?: (line: string) => void): Promise<ReleaseAssets>;
|
|
37
|
+
/**
|
|
38
|
+
* Production download path — a Cognitum-owned, auth-mediated release-proxy
|
|
39
|
+
* endpoint. Not implemented: no such endpoint exists in the confirmed
|
|
40
|
+
* OpenAPI contract today. Throws a clear, specific error rather than
|
|
41
|
+
* silently falling back to the dev path, so a real user hitting this isn't
|
|
42
|
+
* left guessing whether it's their environment or a genuine gap.
|
|
43
|
+
*/
|
|
44
|
+
export declare function downloadReleaseAsset(version: string, triple: TargetTriple, _destDir: string): Promise<ReleaseAssets>;
|
|
45
|
+
/** Entry point install.ts calls — routes to the dev path when explicitly enabled, else fails clearly. */
|
|
46
|
+
export declare function fetchReleaseAssets(version: string, triple: TargetTriple, destDir: string, log?: (line: string) => void): Promise<ReleaseAssets>;
|
|
47
|
+
//# sourceMappingURL=release.d.ts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meta-proxy release resolution + download (ADR-307).
|
|
3
|
+
*
|
|
4
|
+
* Two download paths, deliberately not conflated:
|
|
5
|
+
* - **Production** (`downloadReleaseAsset`): public, signed assets from
|
|
6
|
+
* cognitum-one/meta-proxy-dist. Source remains private; normal users need
|
|
7
|
+
* neither GitHub authentication nor access to the source repository.
|
|
8
|
+
* - **Dev-only** (`downloadViaGhCli`): shells out to `gh release download`,
|
|
9
|
+
* gated behind `RUFLO_DEV_PROXY_INSTALL=1` so it is never reachable by
|
|
10
|
+
* accident, and always logs loudly that it is a developer path.
|
|
11
|
+
*
|
|
12
|
+
* @module proxy/release
|
|
13
|
+
*/
|
|
14
|
+
export const TARGET_TRIPLES = [
|
|
15
|
+
'aarch64-apple-darwin',
|
|
16
|
+
'x86_64-apple-darwin',
|
|
17
|
+
'x86_64-unknown-linux-gnu',
|
|
18
|
+
'aarch64-unknown-linux-gnu',
|
|
19
|
+
'x86_64-pc-windows-msvc',
|
|
20
|
+
];
|
|
21
|
+
export class UnsupportedPlatformError extends Error {
|
|
22
|
+
constructor(platform, arch) {
|
|
23
|
+
super(`meta-proxy has no published release for ${platform}/${arch}`);
|
|
24
|
+
this.name = 'UnsupportedPlatformError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Maps the running Node process's platform/arch onto one of meta-proxy's 5 published triples. */
|
|
28
|
+
export function detectTargetTriple(platform = process.platform, arch = process.arch) {
|
|
29
|
+
if (platform === 'darwin')
|
|
30
|
+
return arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin';
|
|
31
|
+
if (platform === 'linux')
|
|
32
|
+
return arch === 'arm64' ? 'aarch64-unknown-linux-gnu' : 'x86_64-unknown-linux-gnu';
|
|
33
|
+
if (platform === 'win32') {
|
|
34
|
+
if (arch !== 'x64')
|
|
35
|
+
throw new UnsupportedPlatformError(platform, arch);
|
|
36
|
+
return 'x86_64-pc-windows-msvc';
|
|
37
|
+
}
|
|
38
|
+
throw new UnsupportedPlatformError(platform, arch);
|
|
39
|
+
}
|
|
40
|
+
export function releaseArchiveExtension(triple) {
|
|
41
|
+
return triple.endsWith('windows-msvc') ? 'zip' : 'tar.gz';
|
|
42
|
+
}
|
|
43
|
+
export function releaseAssetFilename(version, triple) {
|
|
44
|
+
return `meta-proxy-${version}-${triple}.${releaseArchiveExtension(triple)}`;
|
|
45
|
+
}
|
|
46
|
+
const DEV_INSTALL_ENV = 'RUFLO_DEV_PROXY_INSTALL';
|
|
47
|
+
const RELEASE_SOURCE_ENV = 'RUFLO_PROXY_RELEASE_SOURCE';
|
|
48
|
+
const GH_REPO = 'cognitum-one/meta-proxy';
|
|
49
|
+
const PUBLIC_DIST_BASE = 'https://github.com/cognitum-one/meta-proxy-dist/releases/download';
|
|
50
|
+
const MAX_ARCHIVE_BYTES = 32 * 1024 * 1024;
|
|
51
|
+
async function downloadPublicAsset(url, maxBytes) {
|
|
52
|
+
const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(120_000) });
|
|
53
|
+
if (!response.ok)
|
|
54
|
+
throw new Error(`release download failed: HTTP ${response.status} for ${url}`);
|
|
55
|
+
const declared = Number(response.headers.get('content-length') ?? 0);
|
|
56
|
+
if (declared > maxBytes)
|
|
57
|
+
throw new Error(`release asset exceeds ${maxBytes} byte limit`);
|
|
58
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
59
|
+
if (bytes.length > maxBytes)
|
|
60
|
+
throw new Error(`release asset exceeds ${maxBytes} byte limit`);
|
|
61
|
+
return bytes;
|
|
62
|
+
}
|
|
63
|
+
async function ghExecutor() {
|
|
64
|
+
// Dynamic import, not a static one: @claude-flow/security is only an
|
|
65
|
+
// optionalDependency of this package (see auth/security-bridge.ts for the
|
|
66
|
+
// same reasoning) — a static top-level import would crash module load for
|
|
67
|
+
// any consumer that doesn't have it installed, even ones that never touch
|
|
68
|
+
// this dev-only download path.
|
|
69
|
+
const { SafeExecutor } = await import('@claude-flow/security');
|
|
70
|
+
return new SafeExecutor({ allowedCommands: ['gh'], timeout: 120_000 });
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Dev-only fallback: `gh release download` via SafeExecutor into `destDir`.
|
|
74
|
+
* Requires the caller's environment to already have `gh` authenticated
|
|
75
|
+
* against a GitHub account with access to the private meta-proxy repo — this
|
|
76
|
+
* is NOT something a normal ruflo end user has, which is exactly why this
|
|
77
|
+
* path is gated and logged, not the default.
|
|
78
|
+
*/
|
|
79
|
+
export async function downloadViaGhCli(destDir, version, triple, log = () => { }) {
|
|
80
|
+
const archiveFilename = releaseAssetFilename(version, triple);
|
|
81
|
+
log(`[dev-only] Downloading meta-proxy ${version} (${triple}) via \`gh release download\` — ` +
|
|
82
|
+
'this path is NOT how production installs work; it exists for local development only ' +
|
|
83
|
+
`(gated behind ${DEV_INSTALL_ENV}=1).`);
|
|
84
|
+
const exec = await ghExecutor();
|
|
85
|
+
const result = await exec.execute('gh', [
|
|
86
|
+
'release',
|
|
87
|
+
'download',
|
|
88
|
+
`v${version}`,
|
|
89
|
+
'--repo',
|
|
90
|
+
GH_REPO,
|
|
91
|
+
'--dir',
|
|
92
|
+
destDir,
|
|
93
|
+
'--pattern',
|
|
94
|
+
archiveFilename,
|
|
95
|
+
'--pattern',
|
|
96
|
+
'SHA256SUMS',
|
|
97
|
+
'--pattern',
|
|
98
|
+
'SHA256SUMS.sig',
|
|
99
|
+
'--clobber',
|
|
100
|
+
]);
|
|
101
|
+
if (result.exitCode !== 0) {
|
|
102
|
+
throw new Error(`gh release download failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
|
|
103
|
+
}
|
|
104
|
+
const { readFile } = await import('node:fs/promises');
|
|
105
|
+
const { join } = await import('node:path');
|
|
106
|
+
const [archiveBytes, sumsBytes, sigRaw] = await Promise.all([
|
|
107
|
+
readFile(join(destDir, archiveFilename)),
|
|
108
|
+
readFile(join(destDir, 'SHA256SUMS')),
|
|
109
|
+
readFile(join(destDir, 'SHA256SUMS.sig'), 'utf-8'),
|
|
110
|
+
]);
|
|
111
|
+
return { archiveBytes, archiveFilename, sumsBytes, sigBase64: sigRaw.trim() };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Production download path — a Cognitum-owned, auth-mediated release-proxy
|
|
115
|
+
* endpoint. Not implemented: no such endpoint exists in the confirmed
|
|
116
|
+
* OpenAPI contract today. Throws a clear, specific error rather than
|
|
117
|
+
* silently falling back to the dev path, so a real user hitting this isn't
|
|
118
|
+
* left guessing whether it's their environment or a genuine gap.
|
|
119
|
+
*/
|
|
120
|
+
export async function downloadReleaseAsset(version, triple, _destDir) {
|
|
121
|
+
const archiveFilename = releaseAssetFilename(version, triple);
|
|
122
|
+
const base = (process.env[RELEASE_SOURCE_ENV] || PUBLIC_DIST_BASE).replace(/\/$/, '');
|
|
123
|
+
const release = `${base}/v${encodeURIComponent(version)}`;
|
|
124
|
+
const [archiveBytes, sumsBytes, sigBytes] = await Promise.all([
|
|
125
|
+
downloadPublicAsset(`${release}/${archiveFilename}`, MAX_ARCHIVE_BYTES),
|
|
126
|
+
downloadPublicAsset(`${release}/SHA256SUMS`, 128 * 1024),
|
|
127
|
+
downloadPublicAsset(`${release}/SHA256SUMS.sig`, 16 * 1024),
|
|
128
|
+
]);
|
|
129
|
+
return { archiveBytes, archiveFilename, sumsBytes, sigBase64: sigBytes.toString('utf8').trim() };
|
|
130
|
+
}
|
|
131
|
+
/** Entry point install.ts calls — routes to the dev path when explicitly enabled, else fails clearly. */
|
|
132
|
+
export async function fetchReleaseAssets(version, triple, destDir, log) {
|
|
133
|
+
if (process.env[DEV_INSTALL_ENV] === '1') {
|
|
134
|
+
return downloadViaGhCli(destDir, version, triple, log);
|
|
135
|
+
}
|
|
136
|
+
return downloadReleaseAsset(version, triple, destDir);
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=release.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** ADR-318 access-token-only bridge from ruflo auth to meta-proxy. */
|
|
2
|
+
export declare function removeInjectedToken(): void;
|
|
3
|
+
export declare function refreshInjectedToken(): Promise<boolean>;
|
|
4
|
+
export declare function startTokenRefreshPump(): Promise<() => void>;
|
|
5
|
+
//# sourceMappingURL=token-bridge.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** ADR-318 access-token-only bridge from ruflo auth to meta-proxy. */
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { getValidAccessToken } from '../auth/client.js';
|
|
5
|
+
import { getProfile, listProfiles } from '../auth/state.js';
|
|
6
|
+
import { proxyConfigPath, proxyInjectedTokenPath } from './paths.js';
|
|
7
|
+
const REFRESH_POLL_MS = 30_000;
|
|
8
|
+
function configureInjectedTokenPath() {
|
|
9
|
+
const target = proxyConfigPath();
|
|
10
|
+
fs.mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
|
|
11
|
+
const raw = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : '';
|
|
12
|
+
const line = `ruflo_injected_token_path = ${JSON.stringify(proxyInjectedTokenPath())}`;
|
|
13
|
+
const pattern = /^ruflo_injected_token_path\s*=.*$/m;
|
|
14
|
+
const next = pattern.test(raw) ? raw.replace(pattern, line) : `${raw}${raw && !raw.endsWith('\n') ? '\n' : ''}${line}\n`;
|
|
15
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
16
|
+
fs.writeFileSync(tmp, next, { encoding: 'utf8', mode: 0o600 });
|
|
17
|
+
fs.renameSync(tmp, target);
|
|
18
|
+
}
|
|
19
|
+
export function removeInjectedToken() {
|
|
20
|
+
try {
|
|
21
|
+
fs.unlinkSync(proxyInjectedTokenPath());
|
|
22
|
+
}
|
|
23
|
+
catch { /* absent */ }
|
|
24
|
+
}
|
|
25
|
+
export async function refreshInjectedToken() {
|
|
26
|
+
const { defaultProfile } = listProfiles();
|
|
27
|
+
const profile = getProfile(defaultProfile);
|
|
28
|
+
if (!profile) {
|
|
29
|
+
removeInjectedToken();
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const accessToken = await getValidAccessToken(defaultProfile);
|
|
34
|
+
const current = getProfile(defaultProfile);
|
|
35
|
+
if (!current)
|
|
36
|
+
return false;
|
|
37
|
+
const body = JSON.stringify({ schemaVersion: 1, accessToken, expiresAt: current.accessTokenExpiresAt });
|
|
38
|
+
configureInjectedTokenPath();
|
|
39
|
+
const target = proxyInjectedTokenPath();
|
|
40
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
41
|
+
fs.writeFileSync(tmp, body, { encoding: 'utf8', mode: 0o600 });
|
|
42
|
+
fs.chmodSync(tmp, 0o600);
|
|
43
|
+
fs.renameSync(tmp, target);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
removeInjectedToken();
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function startTokenRefreshPump() {
|
|
52
|
+
await refreshInjectedToken();
|
|
53
|
+
const timer = setInterval(() => void refreshInjectedToken(), REFRESH_POLL_MS);
|
|
54
|
+
const stop = () => {
|
|
55
|
+
clearInterval(timer);
|
|
56
|
+
removeInjectedToken();
|
|
57
|
+
};
|
|
58
|
+
process.once('exit', removeInjectedToken);
|
|
59
|
+
return stop;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=token-bridge.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meta-proxy release verification (ADR-307) — mirrors src/init/helper-signing.ts's
|
|
3
|
+
* raw-EdDSA `crypto.verify(null, ...)` pattern almost exactly, and matches the
|
|
4
|
+
* exact scheme confirmed live 2026-07-16 against a real v0.1.0 release: ONE
|
|
5
|
+
* combined `SHA256SUMS.sig` (raw Ed25519 over the `SHA256SUMS` file's bytes,
|
|
6
|
+
* base64-encoded — not a per-binary signature), then a per-asset SHA-256
|
|
7
|
+
* check against the matching `SHA256SUMS` line. Refuse-all-or-nothing on any
|
|
8
|
+
* mismatch, same discipline as `writeCriticalHelpers()`.
|
|
9
|
+
*
|
|
10
|
+
* @module proxy/verify
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* meta-proxy's committed release-signing public key (SPKI PEM), confirmed
|
|
14
|
+
* live 2026-07-16 by verifying a real `SHA256SUMS.sig` from the v0.1.0
|
|
15
|
+
* release against it (crypto.verify -> true).
|
|
16
|
+
*/
|
|
17
|
+
export declare const PROXY_RELEASE_PUBKEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAjhLDomjIGdcltYC7j+aiESQFD4LWoHaULietG1PuDjw=\n-----END PUBLIC KEY-----";
|
|
18
|
+
export declare class ReleaseVerificationError extends Error {
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
21
|
+
/** Raw EdDSA verify of `SHA256SUMS.sig` (base64) over `SHA256SUMS`'s exact bytes. */
|
|
22
|
+
export declare function verifySha256SumsSignature(sumsBytes: Buffer, sigBase64: string, pubkeyPem?: string): boolean;
|
|
23
|
+
export type ParsedChecksums = Record<string, string>;
|
|
24
|
+
/** Parses `<sha256> <filename>` lines (sha256sum's own output format). */
|
|
25
|
+
export declare function parseSha256Sums(sumsText: string): ParsedChecksums;
|
|
26
|
+
export declare function sha256Hex(bytes: Buffer): string;
|
|
27
|
+
export interface VerifyReleaseInput {
|
|
28
|
+
sumsBytes: Buffer;
|
|
29
|
+
sigBase64: string;
|
|
30
|
+
assetBytes: Buffer;
|
|
31
|
+
assetFilename: string;
|
|
32
|
+
pubkeyPem?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface VerifyReleaseResult {
|
|
35
|
+
sha256: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Full verification: signature over SHA256SUMS, then the asset's own hash
|
|
39
|
+
* against the matching line. Throws `ReleaseVerificationError` on ANY
|
|
40
|
+
* failure — there is no partial-trust outcome, matching ADR-307's "refuses
|
|
41
|
+
* on any mismatch" requirement.
|
|
42
|
+
*/
|
|
43
|
+
export declare function verifyRelease(input: VerifyReleaseInput): VerifyReleaseResult;
|
|
44
|
+
//# sourceMappingURL=verify.d.ts.map
|