claude-flow 3.32.2 → 3.32.9
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 +93 -39
- package/.claude-plugin/hooks/hooks.json +3 -1
- package/.claude-plugin/scripts/ruflo-hook.cjs +2 -2
- package/.claude-plugin/scripts/ruflo-hook.sh +17 -17
- package/package.json +9 -3
- 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 +478 -24
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +43 -1
- 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/funnel/message-transport.d.ts +11 -4
- package/v3/@claude-flow/cli/dist/src/funnel/message-transport.js +11 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +11 -3
- package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.d.ts +9 -0
- package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js +13 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +32 -5
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +72 -0
- 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/package.json +6 -4
- package/.claude/.proven-config-version +0 -1
- package/.claude/proven-config.json +0 -42
|
@@ -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
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-lifetime in-memory access-token cache (ADR-306).
|
|
3
|
+
*
|
|
4
|
+
* The access token is never written to `auth.json` or any other disk file —
|
|
5
|
+
* only the refresh token (via the OS keychain, or nowhere in session-only
|
|
6
|
+
* mode) survives a process exit. Every fresh `ruflo` invocation starts with
|
|
7
|
+
* an empty cache and re-derives an access token from the refresh token (or
|
|
8
|
+
* asks the user to log in again, in session-only mode). This is a deliberate
|
|
9
|
+
* ADR-306 usability cost, not something to "fix" by persisting the access
|
|
10
|
+
* token — see auth/types.ts's doc comment.
|
|
11
|
+
*/
|
|
12
|
+
export declare function setSessionToken(profile: string, accessToken: string, expiresAtMs: number): void;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the cached access token when it remains valid for at least
|
|
15
|
+
* `minValidityMs`. Authenticated callers use a small refresh window so a
|
|
16
|
+
* token cannot expire while an outbound request is in flight.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getSessionToken(profile: string, minValidityMs?: number): string | null;
|
|
19
|
+
export declare function clearSessionToken(profile: string): void;
|
|
20
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-lifetime in-memory access-token cache (ADR-306).
|
|
3
|
+
*
|
|
4
|
+
* The access token is never written to `auth.json` or any other disk file —
|
|
5
|
+
* only the refresh token (via the OS keychain, or nowhere in session-only
|
|
6
|
+
* mode) survives a process exit. Every fresh `ruflo` invocation starts with
|
|
7
|
+
* an empty cache and re-derives an access token from the refresh token (or
|
|
8
|
+
* asks the user to log in again, in session-only mode). This is a deliberate
|
|
9
|
+
* ADR-306 usability cost, not something to "fix" by persisting the access
|
|
10
|
+
* token — see auth/types.ts's doc comment.
|
|
11
|
+
*/
|
|
12
|
+
const sessions = new Map();
|
|
13
|
+
export function setSessionToken(profile, accessToken, expiresAtMs) {
|
|
14
|
+
sessions.set(profile, { accessToken, expiresAt: expiresAtMs });
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns the cached access token when it remains valid for at least
|
|
18
|
+
* `minValidityMs`. Authenticated callers use a small refresh window so a
|
|
19
|
+
* token cannot expire while an outbound request is in flight.
|
|
20
|
+
*/
|
|
21
|
+
export function getSessionToken(profile, minValidityMs = 0) {
|
|
22
|
+
const entry = sessions.get(profile);
|
|
23
|
+
if (!entry)
|
|
24
|
+
return null;
|
|
25
|
+
if (Date.now() + Math.max(0, minValidityMs) >= entry.expiresAt)
|
|
26
|
+
return null;
|
|
27
|
+
return entry.accessToken;
|
|
28
|
+
}
|
|
29
|
+
export function clearSessionToken(profile) {
|
|
30
|
+
sessions.delete(profile);
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `auth.json` persistence — atomic tmp+rename, 0600, under ~/.ruflo (ADR-306).
|
|
3
|
+
* Reuses the exact same primitives `proxy-config.toml`'s consent mirror and
|
|
4
|
+
* every other funnel state file already use (src/funnel/state.ts).
|
|
5
|
+
*/
|
|
6
|
+
import type { AuthFile, ProfileAuthState } from './types.js';
|
|
7
|
+
export declare const DEFAULT_PROFILE = "default";
|
|
8
|
+
export declare function readAuthFile(): AuthFile;
|
|
9
|
+
export declare function getProfile(name?: string): ProfileAuthState | null;
|
|
10
|
+
export declare function listProfiles(): {
|
|
11
|
+
defaultProfile: string;
|
|
12
|
+
profiles: ProfileAuthState[];
|
|
13
|
+
};
|
|
14
|
+
/** Writes/overwrites a profile. The first profile ever written becomes the default. */
|
|
15
|
+
export declare function setProfile(name: string, state: ProfileAuthState, makeDefault?: boolean): void;
|
|
16
|
+
export declare function removeProfile(name: string): boolean;
|
|
17
|
+
/** `auth logout --all` — forgets every profile. */
|
|
18
|
+
export declare function clearAllProfiles(): void;
|
|
19
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `auth.json` persistence — atomic tmp+rename, 0600, under ~/.ruflo (ADR-306).
|
|
3
|
+
* Reuses the exact same primitives `proxy-config.toml`'s consent mirror and
|
|
4
|
+
* every other funnel state file already use (src/funnel/state.ts).
|
|
5
|
+
*/
|
|
6
|
+
import { readStateJson, writeStateJson, deleteStateFile } from '../funnel/state.js';
|
|
7
|
+
const AUTH_FILE = 'auth.json';
|
|
8
|
+
export const DEFAULT_PROFILE = 'default';
|
|
9
|
+
function emptyAuthFile() {
|
|
10
|
+
return { schemaVersion: 1, defaultProfile: DEFAULT_PROFILE, profiles: {} };
|
|
11
|
+
}
|
|
12
|
+
export function readAuthFile() {
|
|
13
|
+
const file = readStateJson(AUTH_FILE);
|
|
14
|
+
return file ?? emptyAuthFile();
|
|
15
|
+
}
|
|
16
|
+
function writeAuthFile(file) {
|
|
17
|
+
writeStateJson(AUTH_FILE, file);
|
|
18
|
+
}
|
|
19
|
+
export function getProfile(name) {
|
|
20
|
+
const file = readAuthFile();
|
|
21
|
+
const key = name ?? file.defaultProfile;
|
|
22
|
+
return file.profiles[key] ?? null;
|
|
23
|
+
}
|
|
24
|
+
export function listProfiles() {
|
|
25
|
+
const file = readAuthFile();
|
|
26
|
+
return { defaultProfile: file.defaultProfile, profiles: Object.values(file.profiles) };
|
|
27
|
+
}
|
|
28
|
+
/** Writes/overwrites a profile. The first profile ever written becomes the default. */
|
|
29
|
+
export function setProfile(name, state, makeDefault = false) {
|
|
30
|
+
const file = readAuthFile();
|
|
31
|
+
const isFirst = Object.keys(file.profiles).length === 0;
|
|
32
|
+
file.profiles[name] = state;
|
|
33
|
+
if (makeDefault || isFirst)
|
|
34
|
+
file.defaultProfile = name;
|
|
35
|
+
writeAuthFile(file);
|
|
36
|
+
}
|
|
37
|
+
export function removeProfile(name) {
|
|
38
|
+
const file = readAuthFile();
|
|
39
|
+
if (!(name in file.profiles))
|
|
40
|
+
return false;
|
|
41
|
+
delete file.profiles[name];
|
|
42
|
+
if (file.defaultProfile === name) {
|
|
43
|
+
const remaining = Object.keys(file.profiles);
|
|
44
|
+
file.defaultProfile = remaining[0] ?? DEFAULT_PROFILE;
|
|
45
|
+
}
|
|
46
|
+
writeAuthFile(file);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/** `auth logout --all` — forgets every profile. */
|
|
50
|
+
export function clearAllProfiles() {
|
|
51
|
+
deleteStateFile(AUTH_FILE);
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo auth` state shapes (ADR-306).
|
|
3
|
+
*
|
|
4
|
+
* `auth.json` holds identity metadata ONLY — never token material. The
|
|
5
|
+
* access token lives exclusively in a process-lifetime in-memory singleton
|
|
6
|
+
* (session.ts); the refresh token goes to the OS keychain (keychainRef set)
|
|
7
|
+
* or nowhere at all when no keychain backend is reachable (keychainRef:
|
|
8
|
+
* null — session-only mode, ADR-306's accepted usability cost, not a bug).
|
|
9
|
+
*/
|
|
10
|
+
export interface ProfileAuthState {
|
|
11
|
+
accountId: string;
|
|
12
|
+
scopes: string[];
|
|
13
|
+
/** ISO timestamp — the access token's own expiry, not tracked beyond this. */
|
|
14
|
+
accessTokenExpiresAt: string;
|
|
15
|
+
/** OS-keychain account identifier for this profile's refresh token, or null if session-only. */
|
|
16
|
+
keychainRef: string | null;
|
|
17
|
+
profile: string;
|
|
18
|
+
loginMethod: 'pkce' | 'device' | 'token-stdin';
|
|
19
|
+
/** ISO timestamp of the login that created/last-refreshed this profile. */
|
|
20
|
+
linkedAt: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AuthFile {
|
|
23
|
+
schemaVersion: 1;
|
|
24
|
+
defaultProfile: string;
|
|
25
|
+
profiles: Record<string, ProfileAuthState>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo auth` state shapes (ADR-306).
|
|
3
|
+
*
|
|
4
|
+
* `auth.json` holds identity metadata ONLY — never token material. The
|
|
5
|
+
* access token lives exclusively in a process-lifetime in-memory singleton
|
|
6
|
+
* (session.ts); the refresh token goes to the OS keychain (keychainRef set)
|
|
7
|
+
* or nowhere at all when no keychain backend is reachable (keychainRef:
|
|
8
|
+
* null — session-only mode, ADR-306's accepted usability cost, not a bug).
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo auth` — Cognitum identity (ADR-306).
|
|
3
|
+
*
|
|
4
|
+
* `login` obtains OAuth tokens via the loopback PKCE flow (default,
|
|
5
|
+
* interactive desktop), the OOB manual-paste flow (`--no-browser`, or
|
|
6
|
+
* auto-detected headless environments), or `--token-stdin` (CI/enterprise
|
|
7
|
+
* automation — refuses interactively otherwise, per ADR-306). The refresh
|
|
8
|
+
* token goes to the OS keychain when reachable, or nowhere (session-only —
|
|
9
|
+
* a deliberate usability cost, not a bug) when it isn't. The access token
|
|
10
|
+
* itself is NEVER written to disk; see src/auth/session.ts.
|
|
11
|
+
*/
|
|
12
|
+
import type { Command } from '../types.js';
|
|
13
|
+
export declare const authCommand: Command;
|
|
14
|
+
export default authCommand;
|
|
15
|
+
//# sourceMappingURL=auth.d.ts.map
|