@cybedefend/vibedefend 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +77 -0
- package/README.md +120 -0
- package/bin/vibedefend.js +19 -0
- package/dist/auth/auth-store.js +170 -0
- package/dist/auth/auth-store.js.map +1 -0
- package/dist/auth/auth.js +125 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/callback-server.js +216 -0
- package/dist/auth/callback-server.js.map +1 -0
- package/dist/auth/pkce.js +31 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/token-exchange.js +83 -0
- package/dist/auth/token-exchange.js.map +1 -0
- package/dist/clients/claude-code.js +170 -0
- package/dist/clients/claude-code.js.map +1 -0
- package/dist/clients/codex.js +378 -0
- package/dist/clients/codex.js.map +1 -0
- package/dist/clients/cursor-guards-rules.js +94 -0
- package/dist/clients/cursor-guards-rules.js.map +1 -0
- package/dist/clients/cursor.js +172 -0
- package/dist/clients/cursor.js.map +1 -0
- package/dist/clients/detect.js +86 -0
- package/dist/clients/detect.js.map +1 -0
- package/dist/clients/registry.js +41 -0
- package/dist/clients/registry.js.map +1 -0
- package/dist/clients/types.js +2 -0
- package/dist/clients/types.js.map +1 -0
- package/dist/clients/vscode.js +187 -0
- package/dist/clients/vscode.js.map +1 -0
- package/dist/clients/windsurf.js +151 -0
- package/dist/clients/windsurf.js.map +1 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/custom-regions.js +112 -0
- package/dist/custom-regions.js.map +1 -0
- package/dist/diagnostics.js +122 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/doctor.js +125 -0
- package/dist/doctor.js.map +1 -0
- package/dist/guards-evaluator/bucketing.js +83 -0
- package/dist/guards-evaluator/bucketing.js.map +1 -0
- package/dist/guards-evaluator/evaluate.js +272 -0
- package/dist/guards-evaluator/evaluate.js.map +1 -0
- package/dist/guards-evaluator/glob.js +148 -0
- package/dist/guards-evaluator/glob.js.map +1 -0
- package/dist/guards-evaluator/index.js +9 -0
- package/dist/guards-evaluator/index.js.map +1 -0
- package/dist/guards-evaluator/preprocess.js +174 -0
- package/dist/guards-evaluator/preprocess.js.map +1 -0
- package/dist/guards-evaluator/redact.js +111 -0
- package/dist/guards-evaluator/redact.js.map +1 -0
- package/dist/guards-evaluator/regex.js +125 -0
- package/dist/guards-evaluator/regex.js.map +1 -0
- package/dist/guards-evaluator/types.js +2 -0
- package/dist/guards-evaluator/types.js.map +1 -0
- package/dist/guards-evaluator/validation.js +115 -0
- package/dist/guards-evaluator/validation.js.map +1 -0
- package/dist/hook-runner.js +6680 -0
- package/dist/hooks/install.js +169 -0
- package/dist/hooks/install.js.map +1 -0
- package/dist/hooks/runtime/api.js +167 -0
- package/dist/hooks/runtime/api.js.map +1 -0
- package/dist/hooks/runtime/config.js +60 -0
- package/dist/hooks/runtime/config.js.map +1 -0
- package/dist/hooks/runtime/emit.js +45 -0
- package/dist/hooks/runtime/emit.js.map +1 -0
- package/dist/hooks/runtime/fetch-rules.js +154 -0
- package/dist/hooks/runtime/fetch-rules.js.map +1 -0
- package/dist/hooks/runtime/guard-rules-cache.js +217 -0
- package/dist/hooks/runtime/guard-rules-cache.js.map +1 -0
- package/dist/hooks/runtime/guard-violations-buffer.js +105 -0
- package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -0
- package/dist/hooks/runtime/pre-compact.js +41 -0
- package/dist/hooks/runtime/pre-compact.js.map +1 -0
- package/dist/hooks/runtime/resolve.js +206 -0
- package/dist/hooks/runtime/resolve.js.map +1 -0
- package/dist/hooks/runtime/session-review.js +198 -0
- package/dist/hooks/runtime/session-review.js.map +1 -0
- package/dist/hooks/runtime/session-start.js +101 -0
- package/dist/hooks/runtime/session-start.js.map +1 -0
- package/dist/hooks/runtime/sniff.js +112 -0
- package/dist/hooks/runtime/sniff.js.map +1 -0
- package/dist/hooks/runtime/types.js +22 -0
- package/dist/hooks/runtime/types.js.map +1 -0
- package/dist/hooks/runtime/user-prompt-submit.js +154 -0
- package/dist/hooks/runtime/user-prompt-submit.js.map +1 -0
- package/dist/index.js +129 -0
- package/dist/index.js.map +1 -0
- package/dist/install.js +183 -0
- package/dist/install.js.map +1 -0
- package/dist/login.js +335 -0
- package/dist/login.js.map +1 -0
- package/dist/prompts.js +134 -0
- package/dist/prompts.js.map +1 -0
- package/dist/self-update.js +177 -0
- package/dist/self-update.js.map +1 -0
- package/dist/status.js +58 -0
- package/dist/status.js.map +1 -0
- package/dist/utils.js +84 -0
- package/dist/utils.js.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +73 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import updateNotifier from 'update-notifier';
|
|
4
|
+
import { runDoctor } from './doctor.js';
|
|
5
|
+
import { runInstall } from './install.js';
|
|
6
|
+
import { runLoginCommand } from './login.js';
|
|
7
|
+
import { selfUpdate } from './self-update.js';
|
|
8
|
+
import { runStatus } from './status.js';
|
|
9
|
+
import { log } from './utils.js';
|
|
10
|
+
import { pkg } from './version.js';
|
|
11
|
+
/**
|
|
12
|
+
* `update-notifier` runs an asynchronous, cached version check against the
|
|
13
|
+
* npm registry. The first call seeds the cache; subsequent invocations
|
|
14
|
+
* within `updateCheckInterval` (24h) are no-ops on the hot path. If a
|
|
15
|
+
* newer version exists, we print a non-blocking banner before commander
|
|
16
|
+
* dispatches the subcommand — the install/update flow still runs
|
|
17
|
+
* normally, the user just sees a nudge.
|
|
18
|
+
*
|
|
19
|
+
* Why not auto-update silently? Two reasons:
|
|
20
|
+
* 1. Global npm installs often need elevated privileges. A surprise
|
|
21
|
+
* `sudo`-required prompt mid-flow is a worse UX than a banner.
|
|
22
|
+
* 2. Self-updating without the user's knowledge is a footgun — they
|
|
23
|
+
* might be deliberately pinning a known-good version while debugging.
|
|
24
|
+
*
|
|
25
|
+
* `--self` (below) does the actual update, on explicit request.
|
|
26
|
+
*/
|
|
27
|
+
const notifier = updateNotifier({
|
|
28
|
+
pkg: { name: pkg.name, version: pkg.version },
|
|
29
|
+
updateCheckInterval: 1000 * 60 * 60 * 24, // once per day
|
|
30
|
+
});
|
|
31
|
+
notifier.notify({
|
|
32
|
+
message: '🛡️ {packageName} update: {currentVersion} → {latestVersion}\n' +
|
|
33
|
+
'Run `vibedefend update --self` to upgrade.',
|
|
34
|
+
defer: false, // print before commander dispatches, not after exit
|
|
35
|
+
isGlobal: true,
|
|
36
|
+
});
|
|
37
|
+
const program = new Command();
|
|
38
|
+
program
|
|
39
|
+
.name('vibedefend')
|
|
40
|
+
.description('VibeDefend — install the CybeDefend MCP, set up Claude Code hooks, manage the business-rules workflow for AI coding agents.')
|
|
41
|
+
.version(pkg.version);
|
|
42
|
+
program
|
|
43
|
+
.command('install')
|
|
44
|
+
.description('Interactive installer: pick a region, register the MCP, configure hooks for each detected agent.')
|
|
45
|
+
.action(async () => {
|
|
46
|
+
try {
|
|
47
|
+
await runInstall();
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
51
|
+
// Inquirer throws `ExitPromptError` when the user hits Ctrl-C. Treat
|
|
52
|
+
// that as a clean exit, not a crash.
|
|
53
|
+
if (message.includes('User force closed') || message.includes('SIGINT')) {
|
|
54
|
+
log.info('Installer cancelled by user.');
|
|
55
|
+
process.exit(130);
|
|
56
|
+
}
|
|
57
|
+
log.err(`Install failed: ${message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
program
|
|
62
|
+
.command('update')
|
|
63
|
+
.description('Re-run the installer to refresh hook scripts. With --self, also upgrade the @cybedefend/vibedefend binary first.')
|
|
64
|
+
.option('--self', 'Update the @cybedefend/vibedefend binary in place before refreshing hooks (npm/pnpm/yarn auto-detected).')
|
|
65
|
+
.option('--version-tag <tag>', 'Version to install when --self is used (default: latest).', 'latest')
|
|
66
|
+
.action(async (opts) => {
|
|
67
|
+
if (opts.self) {
|
|
68
|
+
log.step('Upgrading vibedefend binary');
|
|
69
|
+
const result = selfUpdate({ version: opts.versionTag });
|
|
70
|
+
log.hint(`Resolved from: ${result.resolvedFrom}`);
|
|
71
|
+
log.hint(`Install mode: ${result.mode}`);
|
|
72
|
+
log.info(result.message);
|
|
73
|
+
if (result.attempted && result.exitCode !== 0) {
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
if (result.attempted && result.exitCode === 0) {
|
|
77
|
+
// The currently-running process is still the OLD binary. To pick up
|
|
78
|
+
// the new templates, the user re-runs `vibedefend update` (without
|
|
79
|
+
// --self). Don't re-exec automatically — terminal state + PATH
|
|
80
|
+
// resolution after a global install is fragile across managers
|
|
81
|
+
// (npm/pnpm/yarn each behave slightly differently).
|
|
82
|
+
log.ok('Binary upgraded.');
|
|
83
|
+
log.info('Re-run `vibedefend update` (without --self) to render hooks with the new templates.');
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
// Not attempted (npx-cache, project-local, unknown) — message
|
|
87
|
+
// already told the user what to do. Exit cleanly so we don't
|
|
88
|
+
// proceed to re-render against possibly-stale templates.
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
log.info('Re-running install flow (idempotent).');
|
|
92
|
+
await runInstall();
|
|
93
|
+
});
|
|
94
|
+
program
|
|
95
|
+
.command('login')
|
|
96
|
+
.description('Authenticate against the CybeDefend gateway using OAuth 2.0 Authorization Code + PKCE. ' +
|
|
97
|
+
'Opens your browser to the Logto sign-in page, receives the callback locally, ' +
|
|
98
|
+
'and stores the token bundle (access + refresh) in your OS keychain.')
|
|
99
|
+
.option('--force', 'Re-authenticate even if credentials are already stored.')
|
|
100
|
+
.action(async (opts) => {
|
|
101
|
+
try {
|
|
102
|
+
await runLoginCommand(opts);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
if (message.includes('User force closed') || message.includes('SIGINT')) {
|
|
107
|
+
log.info('Login cancelled by user.');
|
|
108
|
+
process.exit(130);
|
|
109
|
+
}
|
|
110
|
+
log.err(`Login failed: ${message}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
program
|
|
115
|
+
.command('status')
|
|
116
|
+
.description('Show the VibeDefend install state: region, agents, caches, and a live API check.')
|
|
117
|
+
.action(async () => {
|
|
118
|
+
await runStatus();
|
|
119
|
+
});
|
|
120
|
+
program
|
|
121
|
+
.command('doctor')
|
|
122
|
+
.description('Diagnose the VibeDefend install and repair what is fixable.')
|
|
123
|
+
.option('--yes', 'Apply all fixes without the confirmation prompt.')
|
|
124
|
+
.option('--check', 'Report problems only — apply nothing (dry-run).')
|
|
125
|
+
.action(async (opts) => {
|
|
126
|
+
await runDoctor(opts);
|
|
127
|
+
});
|
|
128
|
+
program.parseAsync(process.argv);
|
|
129
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
import { adapterFor } from './clients/registry.js';
|
|
3
|
+
import { checkPlatformSupport } from './clients/claude-code.js';
|
|
4
|
+
import { installHookRuntime } from './hooks/install.js';
|
|
5
|
+
import { runLogin } from './login.js';
|
|
6
|
+
import { hasStoredAuth } from './auth/auth.js';
|
|
7
|
+
import { runInstallPrompts } from './prompts.js';
|
|
8
|
+
import { stampInstalledVersion } from './self-update.js';
|
|
9
|
+
import { detectPlatform, home, log } from './utils.js';
|
|
10
|
+
import { pkg } from './version.js';
|
|
11
|
+
/**
|
|
12
|
+
* Top-level installer.
|
|
13
|
+
*
|
|
14
|
+
* Flow:
|
|
15
|
+
* 1. Banner + platform sanity check (Windows hooks unsupported)
|
|
16
|
+
* 2. Region pick (Production EU / US)
|
|
17
|
+
* 3. Multi-select: which agents to wire hooks into?
|
|
18
|
+
* - shows detection result + version for each
|
|
19
|
+
* - pre-checks supported clients, disables too-old versions
|
|
20
|
+
* 4. Hook config (threshold, auto-propose)
|
|
21
|
+
* 5. For each selected adapter:
|
|
22
|
+
* - (optional) registerMcp — only Claude Code currently
|
|
23
|
+
* - writeSettings — drop hook entries into its config file
|
|
24
|
+
* 6. Write the universal hook scripts to ~/.cybedefend/hooks/
|
|
25
|
+
* (one set serves all adapters)
|
|
26
|
+
* 7. Stamp ~/.cybedefend/version (auto-update plumbing)
|
|
27
|
+
* 8. Print next-steps footer
|
|
28
|
+
*/
|
|
29
|
+
export async function runInstall() {
|
|
30
|
+
banner();
|
|
31
|
+
const p = detectPlatform();
|
|
32
|
+
const platformSupport = checkPlatformSupport();
|
|
33
|
+
log.info(`Platform: ${kleur.bold(p)}`);
|
|
34
|
+
if (!platformSupport.hooksSupported) {
|
|
35
|
+
log.warn(platformSupport.reason ?? 'Hooks unsupported on this platform.');
|
|
36
|
+
}
|
|
37
|
+
const selection = await runInstallPrompts();
|
|
38
|
+
log.step('Region selected: ' + kleur.bold(selection.region.label));
|
|
39
|
+
log.hint(`MCP URL: ${selection.region.mcpUrl}`);
|
|
40
|
+
log.hint(`API base: ${selection.region.apiBase}`);
|
|
41
|
+
if (selection.selectedClients.length === 0) {
|
|
42
|
+
log.info('No agents selected. The MCP URL is printed above — add it to your client manually if needed. ' +
|
|
43
|
+
'The doctrine arrives via Server.instructions on every supported client.');
|
|
44
|
+
stampInstalledVersion(home, pkg.version);
|
|
45
|
+
printDoneFooter([], selection.region.mcpName);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!selection.hooks) {
|
|
49
|
+
// Shouldn't happen — prompts guarantees hooks when selection > 0.
|
|
50
|
+
log.err('Hook config missing despite selected agents — aborting.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
// Install the Node-based hook runner ONCE — one bundled file serves
|
|
54
|
+
// every adapter. The runner reads runtime-config.json at every
|
|
55
|
+
// invocation, so re-running `vibedefend update` propagates new
|
|
56
|
+
// tunables (threshold, auto-propose, region) without re-rendering
|
|
57
|
+
// adapter settings files (they just point at the same runner path).
|
|
58
|
+
if (platformSupport.hooksSupported) {
|
|
59
|
+
log.step('Installing universal hook runner');
|
|
60
|
+
const { runnerPath, configPath } = installHookRuntime({
|
|
61
|
+
region: selection.region,
|
|
62
|
+
hooks: selection.hooks,
|
|
63
|
+
installedVersion: pkg.version,
|
|
64
|
+
});
|
|
65
|
+
log.ok(`Runner: ${runnerPath}`);
|
|
66
|
+
log.hint(`Config: ${configPath}`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
log.warn('Skipping hook runner (platform not supported). MCP registration steps below still run.');
|
|
70
|
+
}
|
|
71
|
+
// Now wire each selected client's settings file. Adapters are
|
|
72
|
+
// responsible for their own idempotency + error handling.
|
|
73
|
+
const installedLabels = [];
|
|
74
|
+
for (const id of selection.selectedClients) {
|
|
75
|
+
const adapter = adapterFor(id);
|
|
76
|
+
log.step(`Wiring ${adapter.label}`);
|
|
77
|
+
if (adapter.registerMcp) {
|
|
78
|
+
adapter.registerMcp({ region: selection.region });
|
|
79
|
+
}
|
|
80
|
+
if (adapter.postRegister) {
|
|
81
|
+
adapter.postRegister({ region: selection.region });
|
|
82
|
+
}
|
|
83
|
+
if (platformSupport.hooksSupported) {
|
|
84
|
+
adapter.writeSettings({
|
|
85
|
+
// hookDir is now legacy — the new design uses a single
|
|
86
|
+
// `~/.cybedefend/hook-runner.js`. Adapters compose `node <path>
|
|
87
|
+
// <subcommand>` via `hookCommand(...)` and ignore this field.
|
|
88
|
+
hookDir: home('.cybedefend'),
|
|
89
|
+
enableSessionReview: selection.hooks.enableSessionReview,
|
|
90
|
+
region: selection.region,
|
|
91
|
+
hooks: selection.hooks,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
installedLabels.push(adapter.label);
|
|
95
|
+
}
|
|
96
|
+
if (selection.hooks.autoProposeMode) {
|
|
97
|
+
log.warn('Auto-propose mode is ON — the agent will call cybe_rules_report_missing ' +
|
|
98
|
+
'without asking you first. You will still review proposals at the next session ' +
|
|
99
|
+
'start (Accept/Reject picker), but they land without a chat-side prompt.');
|
|
100
|
+
}
|
|
101
|
+
stampInstalledVersion(home, pkg.version);
|
|
102
|
+
log.hint(`Version stamp: ~/.cybedefend/version (v${pkg.version})`);
|
|
103
|
+
// Auto-login: if no VibeDefend OAuth bundle exists after hook wiring, run
|
|
104
|
+
// the login flow so the user is fully set up in one command. If login fails,
|
|
105
|
+
// log a warning and continue — the user can run `vibedefend login` later.
|
|
106
|
+
//
|
|
107
|
+
// We check `hasStoredAuth` (which validates OUR specific bundle shape:
|
|
108
|
+
// accessToken + refreshToken + expiresAt + logtoEndpoint + cliAppId +
|
|
109
|
+
// logtoResource) rather than the legacy `resolveToken` resolver. The legacy
|
|
110
|
+
// resolver also matches Codex CLI's own MCP credential (stored under the
|
|
111
|
+
// same keychain service `Codex MCP Credentials` but with a different account
|
|
112
|
+
// shape), which would falsely tell us "already configured" even though our
|
|
113
|
+
// hook can't actually use Codex's MCP-scoped JWT to call the gateway.
|
|
114
|
+
//
|
|
115
|
+
// Note for users who already have Codex set up: yes, you'll see the login
|
|
116
|
+
// flow once. Codex's MCP-scoped token has `aud: cybedefend-mcp` and limited
|
|
117
|
+
// Permify rights; vibedefend's runtime hook needs a USER token (`aud:
|
|
118
|
+
// cybedefend-api`) to call /effective-rules and friends. They are NOT
|
|
119
|
+
// interchangeable. After one Logto SSO consent (instant if your browser
|
|
120
|
+
// already has a session) the refresh_token persists for 14 days.
|
|
121
|
+
if (hasStoredAuth(selection.region.mcpName)) {
|
|
122
|
+
log.ok('VibeDefend credentials already present. Skipping login.');
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
log.info('No VibeDefend credentials found. Running login flow...');
|
|
126
|
+
try {
|
|
127
|
+
await runLogin({
|
|
128
|
+
mcpName: selection.region.mcpName,
|
|
129
|
+
apiBase: selection.region.apiBase,
|
|
130
|
+
logtoEndpoint: selection.region.logtoEndpoint ??
|
|
131
|
+
deriveLogtoEndpoint(selection.region.apiBase),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
136
|
+
log.warn(`Login skipped (${msg}). Run \`vibedefend login\` to authenticate.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
printDoneFooter(installedLabels, selection.region.mcpName);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Derive the Logto OIDC endpoint from a gateway apiBase.
|
|
143
|
+
* Mirrors the logic in login.ts#deriveLogtoEndpointFromApiBase.
|
|
144
|
+
*/
|
|
145
|
+
function deriveLogtoEndpoint(apiBase) {
|
|
146
|
+
if (apiBase.includes('://api.')) {
|
|
147
|
+
return apiBase.replace('://api.', '://auth.');
|
|
148
|
+
}
|
|
149
|
+
if (apiBase.match(/\/\/api-[a-z]+\./)) {
|
|
150
|
+
return apiBase.replace('://api-', '://auth-');
|
|
151
|
+
}
|
|
152
|
+
return apiBase;
|
|
153
|
+
}
|
|
154
|
+
function banner() {
|
|
155
|
+
console.log();
|
|
156
|
+
console.log(kleur.bold().cyan(' VibeDefend ') +
|
|
157
|
+
kleur.dim('— CybeDefend installer for AI coding agents'));
|
|
158
|
+
console.log(kleur.dim(' Sets up the MCP server, hooks, and project link.'));
|
|
159
|
+
console.log();
|
|
160
|
+
}
|
|
161
|
+
function printDoneFooter(installedLabels, mcpName) {
|
|
162
|
+
console.log();
|
|
163
|
+
log.ok('Install complete.');
|
|
164
|
+
console.log();
|
|
165
|
+
if (installedLabels.length > 0) {
|
|
166
|
+
console.log(kleur.bold('Installed hooks for: ') +
|
|
167
|
+
installedLabels.map((l) => kleur.green(l)).join(', '));
|
|
168
|
+
console.log();
|
|
169
|
+
}
|
|
170
|
+
console.log(kleur.bold('Next steps:'));
|
|
171
|
+
console.log(` ${kleur.cyan('1.')} Open your IDE in your repo. First MCP tool call triggers OAuth (browser opens). ` +
|
|
172
|
+
`Tokens are cached in your OS keychain after that.`);
|
|
173
|
+
console.log(` ${kleur.cyan('2.')} Drop a ${kleur.bold('.cybedefend/config.json')} at the repo root with the project UUID:`);
|
|
174
|
+
console.log(kleur.dim(' { "projectId": "<your-project-uuid>" }'));
|
|
175
|
+
console.log(` ${kleur.cyan('3.')} Verify (Claude Code): ${kleur.dim('`claude mcp list`')} should show ${kleur.bold(mcpName)}.`);
|
|
176
|
+
console.log();
|
|
177
|
+
console.log(kleur.dim(' Tunables (set per-shell or per-project to override defaults):'));
|
|
178
|
+
console.log(kleur.dim(' CYBEDEFEND_REVIEW_THRESHOLD — edits before the Stop hook fires'));
|
|
179
|
+
console.log(kleur.dim(' CYBEDEFEND_AUTO_PROPOSE — 1 = auto, 0 = ask user'));
|
|
180
|
+
console.log(kleur.dim(' CYBEDEFEND_PROJECT_ID — override .cybedefend/config.json'));
|
|
181
|
+
console.log();
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vibedefend login` — authenticate against CybeDefend via OAuth 2.0
|
|
3
|
+
* Authorization Code + PKCE (RFC 7636).
|
|
4
|
+
*
|
|
5
|
+
* Flow:
|
|
6
|
+
* 1. Fetch the CLI Logto app_id and API resource from GET /client-apps.
|
|
7
|
+
* 2. Generate a PKCE code_verifier / code_challenge pair (S256).
|
|
8
|
+
* 3. Start a local HTTP callback server on a random unprivileged port
|
|
9
|
+
* (bound to 127.0.0.1 only).
|
|
10
|
+
* 4. Build the Logto /oidc/auth URL with all PKCE + OIDC params.
|
|
11
|
+
* 5. Open the user's default browser. Print fallback URL to terminal.
|
|
12
|
+
* 6. Wait up to 5 minutes for the browser callback (port/callback → code).
|
|
13
|
+
* 7. Exchange code + code_verifier for tokens at /oidc/token.
|
|
14
|
+
* 8. Store full bundle (access, refresh, id tokens + absolute expiry) in
|
|
15
|
+
* the OS keychain (macOS) or ~/.cybedefend/auth.json with mode 0600
|
|
16
|
+
* (Linux/Windows).
|
|
17
|
+
* 9. Pre-seed the guards cache for a warm first hook call.
|
|
18
|
+
* 10. Decode the id_token to extract the user email for the confirmation line.
|
|
19
|
+
*
|
|
20
|
+
* The --token <jwt> flag has been removed. Use `vibedefend login` interactively
|
|
21
|
+
* or obtain a token via the dashboard PAT flow (see PatUsageGuide).
|
|
22
|
+
*/
|
|
23
|
+
import { execFileSync } from 'node:child_process';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
27
|
+
import kleur from 'kleur';
|
|
28
|
+
import { resolveToken } from './hooks/runtime/resolve.js';
|
|
29
|
+
import { log } from './utils.js';
|
|
30
|
+
import { randomBase64url, pkceChallenge } from './auth/pkce.js';
|
|
31
|
+
import { startCallbackServer } from './auth/callback-server.js';
|
|
32
|
+
import { exchangeCodeForTokens } from './auth/token-exchange.js';
|
|
33
|
+
import { storeAuth } from './auth/auth-store.js';
|
|
34
|
+
/**
|
|
35
|
+
* Fetch the CLI OAuth client_id and API resource URL from the public
|
|
36
|
+
* GET /client-apps gateway endpoint.
|
|
37
|
+
*
|
|
38
|
+
* Falls back to `logtoResource` in runtime-config.json if the gateway
|
|
39
|
+
* response doesn't include it (older deployments).
|
|
40
|
+
*/
|
|
41
|
+
export async function fetchClientAppsConfig(apiBase, fetchImpl = globalThis.fetch) {
|
|
42
|
+
let res;
|
|
43
|
+
try {
|
|
44
|
+
res = await fetchImpl(`${apiBase}/client-apps`, {
|
|
45
|
+
method: 'GET',
|
|
46
|
+
signal: AbortSignal.timeout(10_000),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
throw new Error(`Could not reach the gateway at ${apiBase}: ${e.message}`);
|
|
51
|
+
}
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
throw new Error(`GET /client-apps failed: HTTP ${res.status}`);
|
|
54
|
+
}
|
|
55
|
+
const body = (await res.json());
|
|
56
|
+
const cli = body.cli;
|
|
57
|
+
const cliAppId = cli?.appId ?? '';
|
|
58
|
+
if (!cliAppId) {
|
|
59
|
+
throw new Error('GET /client-apps did not return cli.appId — ' +
|
|
60
|
+
'is this gateway version too old?');
|
|
61
|
+
}
|
|
62
|
+
// logtoResource at top level (extended gateway) or fall back to
|
|
63
|
+
// runtime-config.json.
|
|
64
|
+
const logtoResource = body.logtoResource ??
|
|
65
|
+
cli?.resource ??
|
|
66
|
+
loadLogtoResourceFromRuntimeConfig() ??
|
|
67
|
+
'';
|
|
68
|
+
if (!logtoResource) {
|
|
69
|
+
throw new Error('Could not determine the Logto API resource. ' +
|
|
70
|
+
'Add logtoResource to ~/.cybedefend/runtime-config.json or upgrade the gateway.');
|
|
71
|
+
}
|
|
72
|
+
return { cliAppId, logtoResource };
|
|
73
|
+
}
|
|
74
|
+
/** Read logtoResource from the runtime config, if present. */
|
|
75
|
+
function loadLogtoResourceFromRuntimeConfig() {
|
|
76
|
+
const p = join(homedir(), '.cybedefend', 'runtime-config.json');
|
|
77
|
+
if (!existsSync(p))
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
const cfg = JSON.parse(readFileSync(p, 'utf8'));
|
|
81
|
+
return cfg.logtoResource ?? null;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Try to open a URL in the default browser. Best-effort — if `open` (macOS)
|
|
90
|
+
* or `xdg-open` (Linux) isn't available we just return false and the caller
|
|
91
|
+
* prints the fallback URL.
|
|
92
|
+
*/
|
|
93
|
+
export function openBrowser(url) {
|
|
94
|
+
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
95
|
+
try {
|
|
96
|
+
execFileSync(cmd, [url], { stdio: 'ignore', timeout: 3000 });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Reject a promise after `ms` milliseconds with a descriptive timeout error.
|
|
105
|
+
*/
|
|
106
|
+
function timeoutAfter(ms) {
|
|
107
|
+
return new Promise((_, reject) => setTimeout(() => reject(new Error(`Login timed out after ${ms / 60_000} minutes — no callback received.`)), ms));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Decode the `email` claim from an id_token JWT without verifying the
|
|
111
|
+
* signature (the token was just issued by Logto moments ago — verification
|
|
112
|
+
* is important in adversarial contexts, but here we only use it for the
|
|
113
|
+
* confirmation display line).
|
|
114
|
+
*/
|
|
115
|
+
export function decodeIdTokenEmail(idToken) {
|
|
116
|
+
try {
|
|
117
|
+
const parts = idToken.split('.');
|
|
118
|
+
if (parts.length < 2)
|
|
119
|
+
return 'unknown';
|
|
120
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
121
|
+
return typeof payload.email === 'string' ? payload.email : 'unknown';
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return 'unknown';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ─── Guard cache seed ─────────────────────────────────────────────────────────
|
|
128
|
+
/**
|
|
129
|
+
* Seed the guards cache immediately after login so the first tool call has
|
|
130
|
+
* a warm cache and doesn't show the UNVERIFIED banner.
|
|
131
|
+
*/
|
|
132
|
+
async function seedGuardsCache(apiBase, token, fetchImpl = globalThis.fetch) {
|
|
133
|
+
const projectId = resolveProjectIdForLogin();
|
|
134
|
+
if (!projectId)
|
|
135
|
+
return;
|
|
136
|
+
const { fetchEffectiveRulesFromGateway, makeGuardRulesCache } = await import('./hooks/runtime/guard-rules-cache.js');
|
|
137
|
+
const cache = makeGuardRulesCache({
|
|
138
|
+
fetchFn: (ctx) => fetchEffectiveRulesFromGateway(ctx, { fetchImpl }),
|
|
139
|
+
});
|
|
140
|
+
await cache.refreshIfStale({ projectId, token, apiBaseResolved: apiBase });
|
|
141
|
+
}
|
|
142
|
+
/** Read projectId from .cybedefend/config.json in CWD. */
|
|
143
|
+
function resolveProjectIdForLogin() {
|
|
144
|
+
const configPath = join(process.cwd(), '.cybedefend', 'config.json');
|
|
145
|
+
if (!existsSync(configPath))
|
|
146
|
+
return null;
|
|
147
|
+
try {
|
|
148
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
149
|
+
return cfg.projectId ?? null;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function loadRuntimeConfigForLogin() {
|
|
156
|
+
const configPath = join(homedir(), '.cybedefend', 'runtime-config.json');
|
|
157
|
+
if (!existsSync(configPath))
|
|
158
|
+
return null;
|
|
159
|
+
try {
|
|
160
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
161
|
+
const cfg = JSON.parse(raw);
|
|
162
|
+
const region = cfg.region;
|
|
163
|
+
if (!region?.apiBase || !region?.mcpName)
|
|
164
|
+
return null;
|
|
165
|
+
return {
|
|
166
|
+
mcpName: region.mcpName,
|
|
167
|
+
apiBase: region.apiBase,
|
|
168
|
+
logtoEndpoint: region.logtoEndpoint ??
|
|
169
|
+
cfg.logtoEndpoint ??
|
|
170
|
+
undefined,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// ─── Core login logic ─────────────────────────────────────────────────────────
|
|
178
|
+
/**
|
|
179
|
+
* Perform the full OAuth PKCE login flow.
|
|
180
|
+
* Exported for testing and for the install chain.
|
|
181
|
+
*/
|
|
182
|
+
export async function runLogin(opts) {
|
|
183
|
+
const { mcpName, apiBase, logtoEndpoint, force, fetchImpl = globalThis.fetch, openBrowserFn = openBrowser, callbackServerFn = startCallbackServer, } = opts;
|
|
184
|
+
// 1. Already logged in?
|
|
185
|
+
const existingToken = resolveToken(mcpName);
|
|
186
|
+
if (existingToken && !force) {
|
|
187
|
+
log.warn('Already logged in. Use `vibedefend login --force` to re-authenticate.');
|
|
188
|
+
return { email: 'unknown' };
|
|
189
|
+
}
|
|
190
|
+
// 2. Fetch CLI client_id and logtoResource from gateway.
|
|
191
|
+
const { cliAppId, logtoResource } = await fetchClientAppsConfig(apiBase, fetchImpl);
|
|
192
|
+
// 3. PKCE pair.
|
|
193
|
+
const codeVerifier = randomBase64url(64);
|
|
194
|
+
const codeChallenge = pkceChallenge(codeVerifier);
|
|
195
|
+
const state = randomBase64url(16);
|
|
196
|
+
// 4. Start local callback server.
|
|
197
|
+
const { port, codePromise, stopServer } = await callbackServerFn(state);
|
|
198
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
199
|
+
// 5. Build auth URL.
|
|
200
|
+
const authUrl = `${logtoEndpoint}/oidc/auth?` +
|
|
201
|
+
new URLSearchParams({
|
|
202
|
+
client_id: cliAppId,
|
|
203
|
+
response_type: 'code',
|
|
204
|
+
scope: 'openid profile email offline_access',
|
|
205
|
+
// prompt=consent forces Logto to surface the consent screen on EVERY
|
|
206
|
+
// `vibedefend login` and — crucially — to actually issue a refresh_token.
|
|
207
|
+
// Without it, Logto silently SSOs from any pre-existing browser session,
|
|
208
|
+
// skips the offline_access consent step, and omits refresh_token from the
|
|
209
|
+
// /oidc/token response. The user then can't refresh and has to re-login
|
|
210
|
+
// every time the access_token expires. Showing the consent screen once
|
|
211
|
+
// per explicit login (the user typed `vibedefend login`, they expect it)
|
|
212
|
+
// is the right trade-off.
|
|
213
|
+
prompt: 'consent',
|
|
214
|
+
resource: logtoResource,
|
|
215
|
+
redirect_uri: redirectUri,
|
|
216
|
+
code_challenge: codeChallenge,
|
|
217
|
+
code_challenge_method: 'S256',
|
|
218
|
+
state,
|
|
219
|
+
}).toString();
|
|
220
|
+
// 6. Open browser.
|
|
221
|
+
console.log();
|
|
222
|
+
console.log(kleur.bold('VibeDefend Login') + kleur.dim(' — sign in via your browser'));
|
|
223
|
+
console.log();
|
|
224
|
+
const opened = openBrowserFn(authUrl);
|
|
225
|
+
if (!opened) {
|
|
226
|
+
console.log(kleur.yellow(' Could not open the browser automatically.'));
|
|
227
|
+
console.log(` Please open this URL manually:\n ${kleur.cyan(authUrl)}`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(kleur.dim(' If your browser did not open, visit:'));
|
|
231
|
+
console.log(` ${kleur.cyan(authUrl)}`);
|
|
232
|
+
}
|
|
233
|
+
console.log();
|
|
234
|
+
console.log(kleur.dim(' Waiting for you to sign in...'));
|
|
235
|
+
// 7. Wait for callback (5 min timeout).
|
|
236
|
+
let code;
|
|
237
|
+
try {
|
|
238
|
+
code = await Promise.race([codePromise, timeoutAfter(5 * 60 * 1000)]);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
await stopServer();
|
|
242
|
+
}
|
|
243
|
+
// 8. Exchange code + code_verifier for tokens.
|
|
244
|
+
const tokens = await exchangeCodeForTokens({
|
|
245
|
+
logtoEndpoint,
|
|
246
|
+
clientId: cliAppId,
|
|
247
|
+
code,
|
|
248
|
+
codeVerifier,
|
|
249
|
+
redirectUri,
|
|
250
|
+
resource: logtoResource,
|
|
251
|
+
fetchImpl,
|
|
252
|
+
});
|
|
253
|
+
// 9. Compute absolute expiry epoch ms (30s safety margin).
|
|
254
|
+
const expiresAt = Date.now() + (tokens.expires_in - 30) * 1000;
|
|
255
|
+
// 10. Store everything.
|
|
256
|
+
const authBundle = {
|
|
257
|
+
accessToken: tokens.access_token,
|
|
258
|
+
refreshToken: tokens.refresh_token,
|
|
259
|
+
idToken: tokens.id_token,
|
|
260
|
+
expiresAt,
|
|
261
|
+
logtoEndpoint,
|
|
262
|
+
cliAppId,
|
|
263
|
+
logtoResource,
|
|
264
|
+
};
|
|
265
|
+
storeAuth(mcpName, authBundle);
|
|
266
|
+
// 11. Pre-seed the guards cache (best-effort).
|
|
267
|
+
try {
|
|
268
|
+
await seedGuardsCache(apiBase, tokens.access_token, fetchImpl);
|
|
269
|
+
log.hint('Rules cache seeded — Action Guards are ready.');
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// Non-fatal.
|
|
273
|
+
}
|
|
274
|
+
// 12. Confirm.
|
|
275
|
+
const email = decodeIdTokenEmail(tokens.id_token);
|
|
276
|
+
log.ok(`Logged in as ${kleur.bold(email)}`);
|
|
277
|
+
return { email };
|
|
278
|
+
}
|
|
279
|
+
// ─── CLI entry point ──────────────────────────────────────────────────────────
|
|
280
|
+
/**
|
|
281
|
+
* Top-level entry point called by the `vibedefend login` commander action.
|
|
282
|
+
* Loads the region config from ~/.cybedefend/runtime-config.json and
|
|
283
|
+
* delegates to `runLogin`.
|
|
284
|
+
*/
|
|
285
|
+
export async function runLoginCommand(opts) {
|
|
286
|
+
const regionCfg = loadRuntimeConfigForLogin();
|
|
287
|
+
if (!regionCfg) {
|
|
288
|
+
log.err('VibeDefend is not installed yet. Run `vibedefend install` first.');
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
const logtoEndpoint = regionCfg.logtoEndpoint ??
|
|
292
|
+
deriveLogtoEndpointFromApiBase(regionCfg.apiBase);
|
|
293
|
+
try {
|
|
294
|
+
await runLogin({
|
|
295
|
+
mcpName: regionCfg.mcpName,
|
|
296
|
+
apiBase: regionCfg.apiBase,
|
|
297
|
+
logtoEndpoint,
|
|
298
|
+
force: opts.force,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
303
|
+
log.err(message);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
// Belt-and-suspenders: even after stopServer + closeAllConnections, some
|
|
307
|
+
// dependency (axios keep-alive agent, Logto fetch handle, OS DNS resolver)
|
|
308
|
+
// may keep a stray handle alive briefly. The standalone `vibedefend login`
|
|
309
|
+
// CLI command is finished here — explicit exit guarantees the user gets
|
|
310
|
+
// their prompt back immediately.
|
|
311
|
+
process.exit(0);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Derive the Logto OIDC endpoint from the gateway apiBase when not explicitly
|
|
315
|
+
* configured. Convention: api.cybedefend.com → auth.cybedefend.com.
|
|
316
|
+
* Local dev: localhost:3000 → localhost:3003.
|
|
317
|
+
*/
|
|
318
|
+
function deriveLogtoEndpointFromApiBase(apiBase) {
|
|
319
|
+
// Production: https://api.cybedefend.com → https://auth.cybedefend.com
|
|
320
|
+
if (apiBase.includes('://api.')) {
|
|
321
|
+
return apiBase.replace('://api.', '://auth.');
|
|
322
|
+
}
|
|
323
|
+
// Regional: https://api-us.cybedefend.com → https://auth-us.cybedefend.com
|
|
324
|
+
if (apiBase.match(/\/\/api-[a-z]+\./)) {
|
|
325
|
+
return apiBase.replace('://api-', '://auth-');
|
|
326
|
+
}
|
|
327
|
+
// Local dev: http://localhost:3000 (gateway) → http://localhost:3003 (Logto).
|
|
328
|
+
if (apiBase.includes('localhost')) {
|
|
329
|
+
return apiBase.replace(':3000', ':3003');
|
|
330
|
+
}
|
|
331
|
+
// Fallback: assume Logto is at the same host (won't work but at least
|
|
332
|
+
// gives a deterministic URL for the error message).
|
|
333
|
+
return apiBase;
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=login.js.map
|