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.
Files changed (51) hide show
  1. package/.claude/helpers/statusline.cjs +24 -17
  2. package/.claude-plugin/scripts/ruflo-hook.cjs +2 -2
  3. package/.claude-plugin/scripts/ruflo-hook.sh +17 -2
  4. package/package.json +1 -1
  5. package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
  6. package/v3/@claude-flow/cli/dist/src/auth/client.d.ts +89 -0
  7. package/v3/@claude-flow/cli/dist/src/auth/client.js +242 -0
  8. package/v3/@claude-flow/cli/dist/src/auth/constants.d.ts +7 -0
  9. package/v3/@claude-flow/cli/dist/src/auth/constants.js +7 -0
  10. package/v3/@claude-flow/cli/dist/src/auth/scopes.d.ts +14 -0
  11. package/v3/@claude-flow/cli/dist/src/auth/scopes.js +21 -0
  12. package/v3/@claude-flow/cli/dist/src/auth/security-bridge.d.ts +36 -0
  13. package/v3/@claude-flow/cli/dist/src/auth/security-bridge.js +42 -0
  14. package/v3/@claude-flow/cli/dist/src/auth/session.d.ts +20 -0
  15. package/v3/@claude-flow/cli/dist/src/auth/session.js +32 -0
  16. package/v3/@claude-flow/cli/dist/src/auth/state.d.ts +19 -0
  17. package/v3/@claude-flow/cli/dist/src/auth/state.js +53 -0
  18. package/v3/@claude-flow/cli/dist/src/auth/types.d.ts +27 -0
  19. package/v3/@claude-flow/cli/dist/src/auth/types.js +11 -0
  20. package/v3/@claude-flow/cli/dist/src/commands/auth.d.ts +15 -0
  21. package/v3/@claude-flow/cli/dist/src/commands/auth.js +244 -0
  22. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +211 -4
  23. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +20 -9
  24. package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
  25. package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.d.ts +20 -0
  26. package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.js +277 -0
  27. package/v3/@claude-flow/cli/dist/src/commands/proxy.js +97 -4
  28. package/v3/@claude-flow/cli/dist/src/commands/security.js +16 -11
  29. package/v3/@claude-flow/cli/dist/src/funnel/insights.d.ts +1 -0
  30. package/v3/@claude-flow/cli/dist/src/funnel/insights.js +4 -4
  31. package/v3/@claude-flow/cli/dist/src/funnel/local-signals.d.ts +6 -1
  32. package/v3/@claude-flow/cli/dist/src/funnel/local-signals.js +29 -20
  33. package/v3/@claude-flow/cli/dist/src/init/executor.js +2 -2
  34. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +13 -1
  35. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +11 -3
  36. package/v3/@claude-flow/cli/dist/src/proxy/install.d.ts +29 -0
  37. package/v3/@claude-flow/cli/dist/src/proxy/install.js +135 -0
  38. package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.d.ts +61 -0
  39. package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.js +249 -0
  40. package/v3/@claude-flow/cli/dist/src/proxy/paths.d.ts +34 -0
  41. package/v3/@claude-flow/cli/dist/src/proxy/paths.js +70 -0
  42. package/v3/@claude-flow/cli/dist/src/proxy/release.d.ts +47 -0
  43. package/v3/@claude-flow/cli/dist/src/proxy/release.js +138 -0
  44. package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.d.ts +5 -0
  45. package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.js +61 -0
  46. package/v3/@claude-flow/cli/dist/src/proxy/verify.d.ts +44 -0
  47. package/v3/@claude-flow/cli/dist/src/proxy/verify.js +68 -0
  48. package/v3/@claude-flow/cli/dist/src/security/builtin-aidefence.d.ts +34 -0
  49. package/v3/@claude-flow/cli/dist/src/security/builtin-aidefence.js +86 -0
  50. package/v3/@claude-flow/cli/dist/src/services/fable-harness.d.ts +1 -0
  51. package/v3/@claude-flow/cli/package.json +2 -2
@@ -28,6 +28,9 @@ const os = require('os');
28
28
  // Configuration
29
29
  const CONFIG = {
30
30
  maxAgents: 15,
31
+ // Header identity defaults to project/repository name. Set `author` to
32
+ // retain the previous `git config user.name` display (#2682).
33
+ identityMode: (process.env.RUFLO_STATUSLINE_IDENTITY || 'project').toLowerCase(),
31
34
  // Session-cost display. Claude Code's cost.total_cost_usd is a client-side
32
35
  // estimate that "may differ from your actual bill" and reads as misleading on
33
36
  // subscription plans, where token usage is not billed per dollar. These let
@@ -210,7 +213,9 @@ function getStatuslineData() {
210
213
  // 60s TTL (#2337 — don't re-spawn the CLI on every rapid re-render) AND the
211
214
  // tighter promo-rotation clock (this fix — don't let a still-fresh 60s
212
215
  // cache silently freeze the promo/insight row across multiple 20s slots).
213
- if (cache.fresh && cache.promoFresh) return overlayMemoPromo(cache.data);
216
+ if (cache.fresh && cache.promoFresh) {
217
+ return applyLocalOverlays(overlayMemoPromo(cache.data));
218
+ }
214
219
 
215
220
  // #2337: prefer an already-installed CLI bin via direct `node` invocation —
216
221
  // no npx, no registry round-trip, no @latest re-resolve per render. Try
@@ -417,7 +422,7 @@ function buildLocalFallback() {
417
422
  return applyLocalOverlays({
418
423
  user: { name: 'user', gitBranch: '', modelName: 'Claude Code' },
419
424
  v3Progress: { domainsCompleted: 0, totalDomains: 5, dddProgress: 0, patternsLearned: 0, sessionsCompleted: 0 },
420
- security: { status: 'NONE', cvesFixed: 0, totalCves: 0 },
425
+ security: { status: 'NONE', findings: 0, cvesFixed: 0, totalCves: 0 },
421
426
  swarm: { activeAgents: 0, maxAgents: CONFIG.maxAgents, coordinationActive: false },
422
427
  system: { memoryMB: memMB, contextPct: 0, intelligencePct: 0, subAgents: 0 },
423
428
  lastUpdated: new Date().toISOString(),
@@ -476,11 +481,13 @@ function readJSON(filePath) {
476
481
 
477
482
  function getGitInfo() {
478
483
  const result = {
479
- name: 'user', gitBranch: '', modified: 0, untracked: 0,
484
+ name: path.basename(CWD) || 'project', gitBranch: '', modified: 0, untracked: 0,
480
485
  staged: 0, ahead: 0, behind: 0,
481
486
  };
482
487
 
483
488
  const script = [
489
+ 'git rev-parse --show-toplevel 2>/dev/null || pwd',
490
+ 'echo "---SEP---"',
484
491
  'git config user.name 2>/dev/null || echo user',
485
492
  'echo "---SEP---"',
486
493
  'git branch --show-current 2>/dev/null',
@@ -494,12 +501,14 @@ function getGitInfo() {
494
501
  if (!raw) return result;
495
502
 
496
503
  const parts = raw.split('---SEP---').map(function(s) { return s.trim(); });
497
- if (parts.length >= 4) {
498
- result.name = parts[0] || 'user';
499
- result.gitBranch = parts[1] || '';
500
-
501
- if (parts[2]) {
502
- for (const line of parts[2].split('\n')) {
504
+ if (parts.length >= 5) {
505
+ const projectName = path.basename(parts[0] || CWD) || path.basename(CWD) || 'project';
506
+ const authorName = parts[1] || 'user';
507
+ result.name = CONFIG.identityMode === 'author' ? authorName : projectName;
508
+ result.gitBranch = parts[2] || '';
509
+
510
+ if (parts[3]) {
511
+ for (const line of parts[3].split('\n')) {
503
512
  if (!line || line.length < 2) continue;
504
513
  const x = line[0], y = line[1];
505
514
  if (x === '?' && y === '?') { result.untracked++; continue; }
@@ -508,7 +517,7 @@ function getGitInfo() {
508
517
  }
509
518
  }
510
519
 
511
- const ab = (parts[3] || '0 0').split(/\s+/);
520
+ const ab = (parts[4] || '0 0').split(/\s+/);
512
521
  result.ahead = parseInt(ab[0]) || 0;
513
522
  result.behind = parseInt(ab[1]) || 0;
514
523
  }
@@ -722,8 +731,7 @@ function generateStatusline() {
722
731
  const intelligencePct = system.intelligencePct || 0;
723
732
  const memoryMB = system.memoryMB || 0;
724
733
  const subAgents = system.subAgents || 0;
725
- const cvesFixed = security.cvesFixed || 0;
726
- const totalCves = security.totalCves || 0;
734
+ const findings = Math.max(0, security.findings || 0);
727
735
  const secStatus = security.status || 'NONE';
728
736
  const adrCount = adrs.count || 0;
729
737
  const adrImpl = adrs.implemented || 0;
@@ -778,8 +786,7 @@ function generateStatusline() {
778
786
  const hooksColor = hooksEnabled > 0 ? c.brightGreen : c.dim;
779
787
  const intellColor = intelligencePct >= 80 ? c.brightGreen : intelligencePct >= 40 ? c.brightYellow : c.dim;
780
788
  const swarmInd = coordinationActive ? c.brightGreen + '◉' + c.reset + ' ' : c.dim + '○' + c.reset + ' ';
781
- const cvesClean = totalCves === 0 || cvesFixed === totalCves;
782
- const healthAllGreen = (secStatus === 'CLEAN' || secStatus === 'NONE') && cvesClean;
789
+ const healthAllGreen = (secStatus === 'CLEAN' || secStatus === 'NONE') && findings === 0;
783
790
  const opsParts = [];
784
791
  opsParts.push(c.cyan + 'Swarm ' + swarmInd + agentsColor + activeAgents + c.reset + '/' + c.brightWhite + maxAgents + c.reset);
785
792
  if (subAgents > 0) opsParts.push(c.brightPurple + '👥 ' + subAgents + c.reset);
@@ -792,11 +799,11 @@ function generateStatusline() {
792
799
  } else {
793
800
  if (secStatus === 'PENDING') opsParts.push(c.brightYellow + '🛡 scan pending' + c.reset);
794
801
  else if (secStatus === 'IN_PROGRESS') opsParts.push(c.brightYellow + '🛡 scanning…' + c.reset);
802
+ else if (secStatus === 'ISSUES') opsParts.push(c.brightRed + '🛡 findings' + c.reset);
795
803
  else if (secStatus === 'STALE') opsParts.push(c.brightYellow + '🛡 scan stale' + c.reset);
796
804
  else if (secStatus !== 'NONE' && secStatus !== 'CLEAN') opsParts.push(c.brightRed + '🛡 ' + secStatus.toLowerCase() + c.reset);
797
- if (totalCves > 0 && cvesFixed < totalCves) {
798
- const unfixed = totalCves - cvesFixed;
799
- opsParts.push(c.brightRed + '⚠ ' + unfixed + ' CVE' + (unfixed === 1 ? '' : 's') + c.reset);
805
+ if (findings > 0) {
806
+ opsParts.push(c.brightRed + '⚠ ' + findings + ' finding' + (findings === 1 ? '' : 's') + c.reset);
800
807
  }
801
808
  }
802
809
  lines.push(opsParts.join(' ' + c.dim + '·' + c.reset + ' '));
@@ -14,7 +14,7 @@
14
14
  * Behaviour mirrors ruflo-hook.sh:
15
15
  * 1. Reads hook JSON payload from stdin.
16
16
  * 2. Prefers a locally installed `ruflo` or `claude-flow` binary.
17
- * 3. Falls back to `npx --prefer-offline ruflo@alpha`.
17
+ * 3. Falls back to `npx --prefer-offline ruflo@latest`.
18
18
  * 4. Always exits 0 — hook subcommands are best-effort telemetry.
19
19
  * 5. Swallows all stderr — nothing should surface to Claude Code.
20
20
  *
@@ -157,7 +157,7 @@ function main() {
157
157
  // a spurious failure even though the shim itself works correctly.
158
158
  // The bash version doesn't hit this because it backgrounded the work.
159
159
  if (process.env.RUFLO_HOOK_SKIP_NPX !== '1') {
160
- invokeHook('npx', ['--prefer-offline', '--yes', 'ruflo@alpha'], hookArgs, stdinData);
160
+ invokeHook('npx', ['--prefer-offline', '--yes', 'ruflo@latest'], hookArgs, stdinData);
161
161
  }
162
162
 
163
163
  done();
@@ -24,14 +24,29 @@
24
24
  # so redirecting it is a pure cleanup with no functional cost.
25
25
  exec 1>/dev/null 2>/dev/null
26
26
 
27
- run() { "$@" || true; }
27
+ # Bound every hook process. A partially initialized native embedding pool can
28
+ # otherwise keep a logically completed Stop hook alive indefinitely (#2691).
29
+ run() {
30
+ "$@" &
31
+ child=$!
32
+ (
33
+ sleep 15
34
+ kill -TERM "$child" 2>/dev/null || true
35
+ sleep 1
36
+ kill -KILL "$child" 2>/dev/null || true
37
+ ) &
38
+ watchdog=$!
39
+ wait "$child" 2>/dev/null || true
40
+ kill "$watchdog" 2>/dev/null || true
41
+ wait "$watchdog" 2>/dev/null || true
42
+ }
28
43
 
29
44
  if command -v ruflo >/dev/null 2>&1; then
30
45
  run ruflo hooks "$@"
31
46
  elif command -v claude-flow >/dev/null 2>&1; then
32
47
  run claude-flow hooks "$@"
33
48
  else
34
- run npx --prefer-offline --yes ruflo@alpha hooks "$@"
49
+ run npx --prefer-offline --yes ruflo@latest hooks "$@"
35
50
  fi
36
51
 
37
52
  exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.32.1",
3
+ "version": "3.32.8",
4
4
  "description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 1,
4
- "generatedAt": "2026-07-16T23:50:36.870Z",
5
- "gitSha": "81447ce7",
4
+ "generatedAt": "2026-07-18T22:28:26.547Z",
5
+ "gitSha": "fb2f92bb",
6
6
  "catalog": {
7
7
  "agents": 164,
8
8
  "tools": 387,
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `ruflo auth login` flow orchestration (ADR-306) — composes
3
+ * `@claude-flow/security`'s ported OAuth primitives (loopback PKCE, OOB
4
+ * manual-paste, refresh) the same way meta-proxy's `oauth/login.rs`
5
+ * orchestrates its own Rust primitives. See security-bridge.ts for why the
6
+ * package is loaded lazily, and oauth/client.ts (in @claude-flow/security)
7
+ * for why this targets the live `auth.cognitum.one` surface rather than
8
+ * ADR-308's unconfirmed `/v1/auth/*` spec.
9
+ *
10
+ * @module auth/client
11
+ */
12
+ import { type OAuthTokenResponse } from './security-bridge.js';
13
+ /** Refresh before there is less than one minute left on the access token. */
14
+ export declare const ACCESS_TOKEN_REFRESH_WINDOW_MS = 60000;
15
+ export interface LoginResult {
16
+ tokens: OAuthTokenResponse;
17
+ method: 'pkce' | 'device' | 'token-stdin';
18
+ }
19
+ /**
20
+ * Best-effort headlessness signal — a false negative just means the browser
21
+ * flow is attempted and a real browser opens fine anyway; a false positive
22
+ * means the user falls back to the manual OOB flow, which always works
23
+ * regardless. Mirrors meta-proxy's `login.rs::is_probably_headless()`.
24
+ */
25
+ export declare function isProbablyHeadless(): boolean;
26
+ /**
27
+ * Validates the OAuth callback params against the request that started the
28
+ * flow — the CSRF-critical step, extracted so it's independently testable.
29
+ * Order matters: an explicit `error` (user denied) and a missing code are
30
+ * reported as their own reasons; a missing or mismatched `state` is always
31
+ * a state-mismatch, even when a code is present.
32
+ */
33
+ export declare function validateCallback(error: string | null, code: string | null, returnedState: string | null, expectedState: string): {
34
+ ok: true;
35
+ code: string;
36
+ } | {
37
+ ok: false;
38
+ reason: 'denied' | 'state-mismatch';
39
+ detail?: string;
40
+ };
41
+ export declare class LoginCancelledError extends Error {
42
+ constructor();
43
+ }
44
+ export declare class LoginDeniedError extends Error {
45
+ constructor(detail: string);
46
+ }
47
+ export declare class StateMismatchError extends Error {
48
+ constructor();
49
+ }
50
+ export declare class NotLoggedInError extends Error {
51
+ constructor(profile: string);
52
+ }
53
+ export declare class SessionOnlyExpiredError extends Error {
54
+ constructor(profile: string);
55
+ }
56
+ export declare class ScopeConsentMismatchError extends Error {
57
+ constructor(profile: string, scopes: string[]);
58
+ }
59
+ /** Browser-based loopback PKCE login — the ADR-306 default for an interactive desktop. */
60
+ export declare function browserLogin(print: (line: string) => void): Promise<LoginResult>;
61
+ /** Headless fallback: prints the authorize URL with the OOB redirect, prompts for the pasted code. */
62
+ export declare function manualLogin(print: (line: string) => void, input?: NodeJS.ReadableStream): Promise<LoginResult>;
63
+ /**
64
+ * `--token-stdin`: reads one JSON object from stdin,
65
+ * `{access_token, refresh_token?, expires_in, scope}`. Wire format is not
66
+ * specified by ADR-306 — defined here as typed JSON rather than a bare
67
+ * token string, so scope/expiry are explicit rather than inferred.
68
+ */
69
+ export declare function tokenStdinLogin(input?: NodeJS.ReadableStream): Promise<LoginResult>;
70
+ /**
71
+ * Refreshes an access token. Classifies failure into network-unreachable
72
+ * vs. a reachable-but-erroring server so callers can print an honest
73
+ * message instead of collapsing both into "offline" (ADR-308 failure
74
+ * policy: local ruflo functionality is never affected by auth being
75
+ * unavailable, but the diagnostic should say WHY it's unavailable).
76
+ */
77
+ export declare function refreshAccessToken(refreshTokenValue: string): Promise<OAuthTokenResponse>;
78
+ /**
79
+ * Returns an access token suitable for an authenticated call.
80
+ *
81
+ * Fast path: a process-memory token with more than one minute remaining.
82
+ * Slow path: load the profile's refresh token from the OS keychain, perform
83
+ * one refresh, persist a rotated refresh token BEFORE exposing the new access
84
+ * token, then update metadata and the process cache. Refresh is deliberately
85
+ * demand-driven: offline-safe commands such as plain `auth status` never call
86
+ * this function and therefore never create background traffic or retry loops.
87
+ */
88
+ export declare function getValidAccessToken(profileName?: string): Promise<string>;
89
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,242 @@
1
+ /**
2
+ * `ruflo auth login` flow orchestration (ADR-306) — composes
3
+ * `@claude-flow/security`'s ported OAuth primitives (loopback PKCE, OOB
4
+ * manual-paste, refresh) the same way meta-proxy's `oauth/login.rs`
5
+ * orchestrates its own Rust primitives. See security-bridge.ts for why the
6
+ * package is loaded lazily, and oauth/client.ts (in @claude-flow/security)
7
+ * for why this targets the live `auth.cognitum.one` surface rather than
8
+ * ADR-308's unconfirmed `/v1/auth/*` spec.
9
+ *
10
+ * @module auth/client
11
+ */
12
+ import * as readline from 'node:readline/promises';
13
+ import { existsSync } from 'node:fs';
14
+ import { loadSecurityOAuth } from './security-bridge.js';
15
+ import { KEYCHAIN_SERVICE } from './constants.js';
16
+ import { getProfile, setProfile } from './state.js';
17
+ import { getSessionToken, setSessionToken } from './session.js';
18
+ import { hasConsent } from '../funnel/index.js';
19
+ import { domainForScope } from './scopes.js';
20
+ /** Refresh before there is less than one minute left on the access token. */
21
+ export const ACCESS_TOKEN_REFRESH_WINDOW_MS = 60_000;
22
+ /**
23
+ * Best-effort headlessness signal — a false negative just means the browser
24
+ * flow is attempted and a real browser opens fine anyway; a false positive
25
+ * means the user falls back to the manual OOB flow, which always works
26
+ * regardless. Mirrors meta-proxy's `login.rs::is_probably_headless()`.
27
+ */
28
+ export function isProbablyHeadless() {
29
+ return Boolean(process.env.SSH_CONNECTION || process.env.SSH_TTY || process.env.CONTAINER || existsDockerEnv());
30
+ }
31
+ function existsDockerEnv() {
32
+ try {
33
+ return existsSync('/.dockerenv');
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ /**
40
+ * Validates the OAuth callback params against the request that started the
41
+ * flow — the CSRF-critical step, extracted so it's independently testable.
42
+ * Order matters: an explicit `error` (user denied) and a missing code are
43
+ * reported as their own reasons; a missing or mismatched `state` is always
44
+ * a state-mismatch, even when a code is present.
45
+ */
46
+ export function validateCallback(error, code, returnedState, expectedState) {
47
+ if (error)
48
+ return { ok: false, reason: 'denied', detail: error };
49
+ if (!code)
50
+ return { ok: false, reason: 'denied', detail: 'no authorization code received' };
51
+ if (!returnedState || returnedState !== expectedState)
52
+ return { ok: false, reason: 'state-mismatch' };
53
+ return { ok: true, code };
54
+ }
55
+ export class LoginCancelledError extends Error {
56
+ constructor() {
57
+ super('login cancelled: no code was entered');
58
+ this.name = 'LoginCancelledError';
59
+ }
60
+ }
61
+ export class LoginDeniedError extends Error {
62
+ constructor(detail) {
63
+ super(`authorization was denied or failed: ${detail}`);
64
+ this.name = 'LoginDeniedError';
65
+ }
66
+ }
67
+ export class StateMismatchError extends Error {
68
+ constructor() {
69
+ super('state mismatch — the OAuth callback did not match the request this CLI sent');
70
+ this.name = 'StateMismatchError';
71
+ }
72
+ }
73
+ export class NotLoggedInError extends Error {
74
+ constructor(profile) {
75
+ super(`not logged in for profile "${profile}" — run: ruflo auth login --profile ${profile}`);
76
+ this.name = 'NotLoggedInError';
77
+ }
78
+ }
79
+ export class SessionOnlyExpiredError extends Error {
80
+ constructor(profile) {
81
+ super(`profile "${profile}" has no persisted refresh token and its in-memory access token is ` +
82
+ `absent or expiring — run: ruflo auth login --profile ${profile}`);
83
+ this.name = 'SessionOnlyExpiredError';
84
+ }
85
+ }
86
+ export class ScopeConsentMismatchError extends Error {
87
+ constructor(profile, scopes) {
88
+ super(`profile "${profile}" has scope(s) without matching local consent: ${scopes.join(', ')} — ` +
89
+ 'authenticated capability denied');
90
+ this.name = 'ScopeConsentMismatchError';
91
+ }
92
+ }
93
+ /** Browser-based loopback PKCE login — the ADR-306 default for an interactive desktop. */
94
+ export async function browserLogin(print) {
95
+ const sec = await loadSecurityOAuth();
96
+ const server = await sec.CallbackServer.bind();
97
+ const pkce = sec.generatePkce();
98
+ const url = sec.authorizeUrl(server.redirectUri, pkce.state, pkce.codeChallenge);
99
+ print('Opening your browser to sign in to Cognitum...');
100
+ print(`If it doesn't open automatically, visit:\n\n ${url}\n`);
101
+ await sec.openBrowser(url).catch(() => { }); // best-effort — the URL above is always the fallback
102
+ print('Waiting for you to finish signing in...');
103
+ const result = await server.awaitCallback();
104
+ const validated = validateCallback(result.error, result.code, result.state, pkce.state);
105
+ if (!validated.ok) {
106
+ if (validated.reason === 'state-mismatch')
107
+ throw new StateMismatchError();
108
+ throw new LoginDeniedError(validated.detail ?? 'unknown');
109
+ }
110
+ const tokens = await sec.exchangeCode(validated.code, pkce.codeVerifier, server.redirectUri);
111
+ return { tokens, method: 'pkce' };
112
+ }
113
+ /** Headless fallback: prints the authorize URL with the OOB redirect, prompts for the pasted code. */
114
+ export async function manualLogin(print, input = process.stdin) {
115
+ const sec = await loadSecurityOAuth();
116
+ print('Browser-based callback unavailable (SSH/container detected, or --no-browser).\n');
117
+ const pkce = sec.generatePkce();
118
+ const url = sec.authorizeUrl(sec.OOB_REDIRECT_URI, pkce.state, pkce.codeChallenge);
119
+ print(`Open this URL in a browser and authorize:\n\n ${url}\n`);
120
+ // `rl.question()` resolves on a newline-terminated 'line' event — if `input`
121
+ // ends without ever emitting one (e.g. stdin closed early, or piped input
122
+ // with no trailing newline), it hangs forever rather than treating EOF as
123
+ // a cancellation. Race it against the interface's own 'close' event so an
124
+ // early EOF resolves to "" (-> LoginCancelledError below) instead of hanging.
125
+ const rl = readline.createInterface({ input, terminal: false });
126
+ let code;
127
+ try {
128
+ const closed = new Promise((resolve) => rl.once('close', () => resolve('')));
129
+ code = (await Promise.race([rl.question('Paste the code shown after authorizing: '), closed])).trim();
130
+ }
131
+ finally {
132
+ rl.close();
133
+ }
134
+ if (!code)
135
+ throw new LoginCancelledError();
136
+ const tokens = await sec.exchangeManualCode(code, pkce.codeVerifier);
137
+ return { tokens, method: 'device' };
138
+ }
139
+ /**
140
+ * `--token-stdin`: reads one JSON object from stdin,
141
+ * `{access_token, refresh_token?, expires_in, scope}`. Wire format is not
142
+ * specified by ADR-306 — defined here as typed JSON rather than a bare
143
+ * token string, so scope/expiry are explicit rather than inferred.
144
+ */
145
+ export async function tokenStdinLogin(input = process.stdin) {
146
+ const chunks = [];
147
+ for await (const chunk of input)
148
+ chunks.push(chunk);
149
+ const raw = Buffer.concat(chunks).toString('utf-8').trim();
150
+ if (!raw)
151
+ throw new Error('--token-stdin: no input received on stdin');
152
+ let parsed;
153
+ try {
154
+ parsed = JSON.parse(raw);
155
+ }
156
+ catch {
157
+ throw new Error('--token-stdin expects a single JSON object: {"access_token","refresh_token"?,"expires_in","scope"}');
158
+ }
159
+ if (!parsed.access_token)
160
+ throw new Error('--token-stdin: JSON is missing required field "access_token"');
161
+ const tokens = {
162
+ access_token: parsed.access_token,
163
+ token_type: 'Bearer',
164
+ refresh_token: parsed.refresh_token,
165
+ expires_in: parsed.expires_in,
166
+ };
167
+ return { tokens, method: 'token-stdin' };
168
+ }
169
+ /**
170
+ * Refreshes an access token. Classifies failure into network-unreachable
171
+ * vs. a reachable-but-erroring server so callers can print an honest
172
+ * message instead of collapsing both into "offline" (ADR-308 failure
173
+ * policy: local ruflo functionality is never affected by auth being
174
+ * unavailable, but the diagnostic should say WHY it's unavailable).
175
+ */
176
+ export async function refreshAccessToken(refreshTokenValue) {
177
+ const sec = await loadSecurityOAuth();
178
+ try {
179
+ return await sec.refreshToken(refreshTokenValue);
180
+ }
181
+ catch (e) {
182
+ if (e instanceof sec.OAuthError) {
183
+ if (e.code === 'network') {
184
+ throw new Error('Could not reach the Cognitum auth service. ruflo core functionality is unaffected — ' +
185
+ 'sign-in is not required for local use.');
186
+ }
187
+ throw new Error(`Cognitum auth service returned an unexpected response: ${e.message}`);
188
+ }
189
+ throw e;
190
+ }
191
+ }
192
+ /**
193
+ * Returns an access token suitable for an authenticated call.
194
+ *
195
+ * Fast path: a process-memory token with more than one minute remaining.
196
+ * Slow path: load the profile's refresh token from the OS keychain, perform
197
+ * one refresh, persist a rotated refresh token BEFORE exposing the new access
198
+ * token, then update metadata and the process cache. Refresh is deliberately
199
+ * demand-driven: offline-safe commands such as plain `auth status` never call
200
+ * this function and therefore never create background traffic or retry loops.
201
+ */
202
+ export async function getValidAccessToken(profileName = 'default') {
203
+ const profile = getProfile(profileName);
204
+ if (!profile)
205
+ throw new NotLoggedInError(profileName);
206
+ const scopesWithoutConsent = profile.scopes.filter((scope) => {
207
+ const domain = domainForScope(scope);
208
+ return domain !== undefined && !hasConsent(domain);
209
+ });
210
+ if (scopesWithoutConsent.length > 0) {
211
+ throw new ScopeConsentMismatchError(profileName, scopesWithoutConsent);
212
+ }
213
+ const cached = getSessionToken(profileName, ACCESS_TOKEN_REFRESH_WINDOW_MS);
214
+ if (cached)
215
+ return cached;
216
+ if (!profile.keychainRef)
217
+ throw new SessionOnlyExpiredError(profileName);
218
+ const sec = await loadSecurityOAuth();
219
+ const keychain = await sec.createKeychainAdapter();
220
+ const refreshTokenValue = await keychain.getSecret(KEYCHAIN_SERVICE, profile.keychainRef);
221
+ if (!refreshTokenValue)
222
+ throw new SessionOnlyExpiredError(profileName);
223
+ const refreshed = await refreshAccessToken(refreshTokenValue);
224
+ if (!refreshed.access_token)
225
+ throw new Error('Cognitum refresh response did not contain an access token');
226
+ // Cognitum rotates refresh tokens with reuse detection. Commit the rotated
227
+ // credential first; if this write fails, do not publish/cache the access
228
+ // token and do not retry the already-spent old refresh token here.
229
+ if (refreshed.refresh_token) {
230
+ await keychain.setSecret(KEYCHAIN_SERVICE, profile.keychainRef, refreshed.refresh_token);
231
+ }
232
+ const expiresAtMs = Date.now() + Math.max(0, refreshed.expires_in ?? 0) * 1000;
233
+ setSessionToken(profileName, refreshed.access_token, expiresAtMs);
234
+ setProfile(profileName, {
235
+ ...profile,
236
+ accountId: refreshed.account_email ?? profile.accountId,
237
+ accessTokenExpiresAt: new Date(expiresAtMs).toISOString(),
238
+ linkedAt: new Date().toISOString(),
239
+ });
240
+ return refreshed.access_token;
241
+ }
242
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shared ADR-306 constants — split out so `commands/auth.ts` and
3
+ * `auth/client.ts` reference the same keychain service name rather than two
4
+ * independently-typed string literals that could silently drift apart.
5
+ */
6
+ export declare const KEYCHAIN_SERVICE = "ruflo-cognitum-auth";
7
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shared ADR-306 constants — split out so `commands/auth.ts` and
3
+ * `auth/client.ts` reference the same keychain service name rather than two
4
+ * independently-typed string literals that could silently drift apart.
5
+ */
6
+ export const KEYCHAIN_SERVICE = 'ruflo-cognitum-auth';
7
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ADR-306 scope <-> ADR-302 consent-domain mapping.
3
+ *
4
+ * The 5 ADR-306 scopes map 1:1 onto 5 `ConsentDomain` values that already
5
+ * exist in src/funnel/types.ts (pre-provisioned for exactly this purpose —
6
+ * see that file's own comments). No new consent machinery is needed; this
7
+ * module is just the lookup table ADR-306 describes but never names in code.
8
+ */
9
+ import type { ConsentDomain } from '../funnel/index.js';
10
+ export declare const SCOPE_TO_DOMAIN: Record<string, ConsentDomain>;
11
+ export declare function domainForScope(scope: string): ConsentDomain | undefined;
12
+ /** `login` always requests this scope only, on first run — ADR-306. */
13
+ export declare const INITIAL_SCOPE = "account.create";
14
+ //# sourceMappingURL=scopes.d.ts.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * ADR-306 scope <-> ADR-302 consent-domain mapping.
3
+ *
4
+ * The 5 ADR-306 scopes map 1:1 onto 5 `ConsentDomain` values that already
5
+ * exist in src/funnel/types.ts (pre-provisioned for exactly this purpose —
6
+ * see that file's own comments). No new consent machinery is needed; this
7
+ * module is just the lookup table ADR-306 describes but never names in code.
8
+ */
9
+ export const SCOPE_TO_DOMAIN = {
10
+ 'account.create': 'account',
11
+ 'proxy.use': 'proxy-install',
12
+ 'cloud.route': 'cloud-routing',
13
+ 'telemetry.write': 'telemetry',
14
+ 'hosted.memory.use': 'hosted-memory',
15
+ };
16
+ export function domainForScope(scope) {
17
+ return SCOPE_TO_DOMAIN[scope];
18
+ }
19
+ /** `login` always requests this scope only, on first run — ADR-306. */
20
+ export const INITIAL_SCOPE = 'account.create';
21
+ //# sourceMappingURL=scopes.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Lazy loader for `@claude-flow/security`'s OAuth + keychain surface
3
+ * (ADR-306). `@claude-flow/security` is an `optionalDependency` of this
4
+ * package (see package.json) — `ruflo auth` is the one capability that
5
+ * genuinely cannot function without it, so this module's job is turning "the
6
+ * optional package failed to install" into one clear, actionable error
7
+ * instead of a raw `ERR_MODULE_NOT_FOUND` stack trace. Mirrors the
8
+ * lazy-`require`-in-try/catch idiom already used for this same package in
9
+ * src/mcp-client.ts's `applyContentBoundaryGuardrail`, but as an async
10
+ * dynamic `import()` (the security package is itself `"type": "module"`).
11
+ *
12
+ * Only `import type` is used for the package's real types below — type-only
13
+ * imports are fully erased at compile time and never touch the runtime
14
+ * module resolver, so this file type-checks whether or not the optional
15
+ * dependency happens to be installed in a given consumer's node_modules.
16
+ */
17
+ import type { authorizeUrl as AuthorizeUrlFn, exchangeCode as ExchangeCodeFn, refreshToken as RefreshTokenFn, exchangeManualCode as ExchangeManualCodeFn, generatePkce as GeneratePkceFn, OAuthTokenResponse, PkceRequest, CallbackServer as CallbackServerClass, CallbackResult, openBrowser as OpenBrowserFn, createKeychainAdapter as CreateKeychainAdapterFn, KeychainAdapter, OAuthError as OAuthErrorClass } from '@claude-flow/security';
18
+ export interface SecurityOAuthModule {
19
+ authorizeUrl: typeof AuthorizeUrlFn;
20
+ exchangeCode: typeof ExchangeCodeFn;
21
+ refreshToken: typeof RefreshTokenFn;
22
+ exchangeManualCode: typeof ExchangeManualCodeFn;
23
+ generatePkce: typeof GeneratePkceFn;
24
+ CallbackServer: typeof CallbackServerClass;
25
+ openBrowser: typeof OpenBrowserFn;
26
+ createKeychainAdapter: typeof CreateKeychainAdapterFn;
27
+ OAuthError: typeof OAuthErrorClass;
28
+ OOB_REDIRECT_URI: string;
29
+ }
30
+ export type { OAuthTokenResponse, PkceRequest, CallbackResult, KeychainAdapter };
31
+ export declare class SecurityPackageMissingError extends Error {
32
+ constructor(cause: unknown);
33
+ }
34
+ /** Loads `@claude-flow/security`'s OAuth surface, throwing a clear error if it's absent. */
35
+ export declare function loadSecurityOAuth(): Promise<SecurityOAuthModule>;
36
+ //# sourceMappingURL=security-bridge.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Lazy loader for `@claude-flow/security`'s OAuth + keychain surface
3
+ * (ADR-306). `@claude-flow/security` is an `optionalDependency` of this
4
+ * package (see package.json) — `ruflo auth` is the one capability that
5
+ * genuinely cannot function without it, so this module's job is turning "the
6
+ * optional package failed to install" into one clear, actionable error
7
+ * instead of a raw `ERR_MODULE_NOT_FOUND` stack trace. Mirrors the
8
+ * lazy-`require`-in-try/catch idiom already used for this same package in
9
+ * src/mcp-client.ts's `applyContentBoundaryGuardrail`, but as an async
10
+ * dynamic `import()` (the security package is itself `"type": "module"`).
11
+ *
12
+ * Only `import type` is used for the package's real types below — type-only
13
+ * imports are fully erased at compile time and never touch the runtime
14
+ * module resolver, so this file type-checks whether or not the optional
15
+ * dependency happens to be installed in a given consumer's node_modules.
16
+ */
17
+ export class SecurityPackageMissingError extends Error {
18
+ constructor(cause) {
19
+ super("ruflo auth needs the '@claude-flow/security' package, which isn't installed " +
20
+ "(it's an optional dependency — install/reinstall failed or was skipped for this " +
21
+ `platform). Try: npm install @claude-flow/security. Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`);
22
+ this.name = 'SecurityPackageMissingError';
23
+ }
24
+ }
25
+ let cached = null;
26
+ /** Loads `@claude-flow/security`'s OAuth surface, throwing a clear error if it's absent. */
27
+ export async function loadSecurityOAuth() {
28
+ if (cached)
29
+ return cached;
30
+ try {
31
+ const mod = (await import('@claude-flow/security'));
32
+ if (!mod.authorizeUrl || !mod.createKeychainAdapter) {
33
+ throw new Error('module loaded but is missing expected OAuth exports');
34
+ }
35
+ cached = mod;
36
+ return mod;
37
+ }
38
+ catch (e) {
39
+ throw new SecurityPackageMissingError(e);
40
+ }
41
+ }
42
+ //# sourceMappingURL=security-bridge.js.map