@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/LICENSE
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Licensor: CybeDefend
|
|
4
|
+
Licensed Work: @cybedefend/vibedefend
|
|
5
|
+
Additional Use Grant: Production use is permitted except to offer a service that competes with CybeDefend.
|
|
6
|
+
Change Date: 2030-05-25
|
|
7
|
+
Change License: Apache-2.0
|
|
8
|
+
|
|
9
|
+
License text copyright (c) 2024 MariaDB plc, All Rights Reserved.
|
|
10
|
+
"Business Source License" is a trademark of MariaDB plc.
|
|
11
|
+
|
|
12
|
+
Terms
|
|
13
|
+
|
|
14
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
15
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
16
|
+
Licensor may make an Additional Use Grant, above, permitting limited production
|
|
17
|
+
use.
|
|
18
|
+
|
|
19
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
20
|
+
available distribution of a specific version of the Licensed Work under this
|
|
21
|
+
License, whichever comes first, the Licensor hereby grants you rights under the
|
|
22
|
+
terms of the Change License, and the rights granted in the paragraph above
|
|
23
|
+
terminate.
|
|
24
|
+
|
|
25
|
+
If your use of the Licensed Work does not comply with the requirements currently
|
|
26
|
+
in effect as described in this License, you must purchase a commercial license
|
|
27
|
+
from the Licensor, its affiliated entities, or authorized resellers, or you must
|
|
28
|
+
refrain from using the Licensed Work.
|
|
29
|
+
|
|
30
|
+
All copies of the original and modified Licensed Work, and derivative works of
|
|
31
|
+
the Licensed Work, are subject to this License. This License applies separately
|
|
32
|
+
for each version of the Licensed Work and the Change Date may vary for each
|
|
33
|
+
version of the Licensed Work released by Licensor.
|
|
34
|
+
|
|
35
|
+
You must conspicuously display this License on each original or modified copy
|
|
36
|
+
of the Licensed Work. If you receive the Licensed Work in original or modified
|
|
37
|
+
form from a third party, the terms and conditions set forth in this License
|
|
38
|
+
apply to your use of that work.
|
|
39
|
+
|
|
40
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
41
|
+
terminate your rights under this License for the current and all other versions
|
|
42
|
+
of the Licensed Work.
|
|
43
|
+
|
|
44
|
+
This License does not grant you any right in any trademark or logo of Licensor
|
|
45
|
+
or its affiliates (provided that you may use a trademark or logo of Licensor as
|
|
46
|
+
expressly required by this License).
|
|
47
|
+
|
|
48
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
|
|
49
|
+
"AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS
|
|
50
|
+
OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY,
|
|
51
|
+
FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.
|
|
52
|
+
|
|
53
|
+
MariaDB hereby grants you permission to use this License's text to license your
|
|
54
|
+
works, and to refer to it using the trademark "Business Source License", as
|
|
55
|
+
long as you comply with the Covenants of Licensor below.
|
|
56
|
+
|
|
57
|
+
Covenants of Licensor
|
|
58
|
+
|
|
59
|
+
In consideration of the right to use this License's text and the "Business
|
|
60
|
+
Source License" name and trademark, Licensor covenants to MariaDB, and to all
|
|
61
|
+
other recipients of the licensed work to be provided by Licensor:
|
|
62
|
+
|
|
63
|
+
To specify as the Change License the GPL Version 2.0 or any later version, or a
|
|
64
|
+
license that is compatible with GPL Version 2.0 or a later version, where
|
|
65
|
+
"compatible" means that software provided under the Change License can be
|
|
66
|
+
included in a program with software provided under GPL Version 2.0 or a later
|
|
67
|
+
version. Licensor may specify additional Change Licenses without limitation.
|
|
68
|
+
|
|
69
|
+
To either: (a) specify an additional grant of rights to use that does not impose
|
|
70
|
+
any additional restriction on the right granted in this License, as the
|
|
71
|
+
Additional Use Grant; or (b) insert the text "None".
|
|
72
|
+
|
|
73
|
+
Notice
|
|
74
|
+
|
|
75
|
+
The Business Source License (this document, or the "License") is not an Open
|
|
76
|
+
Source license. However, the Licensed Work will eventually be made available
|
|
77
|
+
under an Open Source License, as stated in this License.
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# `@cybedefend/vibedefend`
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@cybedefend/vibedefend)
|
|
4
|
+
[](https://nodejs.org)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
> **VibeDefend** — one-shot installer that connects your AI coding agent
|
|
8
|
+
> to CybeDefend. Each time the agent edits code, the right business and
|
|
9
|
+
> security rules for the change land in its context. Each time you commit
|
|
10
|
+
> work, a quick gap analysis catches the rules you never wrote down.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx -y @cybedefend/vibedefend@latest install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Works on **macOS, Linux, and Windows** (PowerShell, cmd, bash, zsh, fish,
|
|
19
|
+
Git Bash — pick any). Requires **Node 18.17 or later** — most users already
|
|
20
|
+
have it because Claude Code / Cursor / Codex ship with a bundled Node.
|
|
21
|
+
|
|
22
|
+
The installer is fully interactive: pick a region, pick which agents to
|
|
23
|
+
wire (auto-detected), confirm. That's it.
|
|
24
|
+
|
|
25
|
+
Prefer a global install?
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g @cybedefend/vibedefend && vibedefend install
|
|
29
|
+
# or pnpm / yarn — same package
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Supported agents
|
|
33
|
+
|
|
34
|
+
VibeDefend auto-detects and wires whichever of these you have installed.
|
|
35
|
+
|
|
36
|
+
| Capability | Claude Code | Cursor | OpenAI Codex | Windsurf | VS Code Copilot |
|
|
37
|
+
|---|:---:|:---:|:---:|:---:|:---:|
|
|
38
|
+
| **MCP server install** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
39
|
+
| **Business + Security Rules** *(injected pre-edit)* | ✅ | ✅ | ✅ | ⚠️ writes only | ✅ |
|
|
40
|
+
| **Action Guards** *(hard block on deny)* | ✅ all tools | ✅ all tools | ✅ all tools | ⚠️ writes + MCP fallback¹ | ❌ not yet wired |
|
|
41
|
+
| **Session Start** *(loads doctrine + proposals inbox)* | ✅ | ✅ | ✅ | ⚠️ proxied² | ✅ |
|
|
42
|
+
| **Session Review** *(end-of-session gap analysis)* | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
43
|
+
| **PreCompact** *(long-session gap analysis)* | ✅ | ✅ | ❌ no event | ❌ no event | ✅ |
|
|
44
|
+
| **Doctrine backstop** *(per-prompt reminder)* | ✅ | ❌ | ✱ via MCP³ | ❌ | ❌ |
|
|
45
|
+
| **Min version** | latest | ≥ 1.7 | latest | latest | ≥ 1.110 |
|
|
46
|
+
|
|
47
|
+
**Legend** — ✅ supported · ⚠️ supported with caveats · ❌ not exposed by the agent · ✱ alternate mechanism
|
|
48
|
+
|
|
49
|
+
¹ Windsurf's `pre_write_code` hook hard-blocks on file writes only. For non-write tool calls (Read / Bash / WebFetch) the installer drops a snippet into `.windsurfrules` instructing the agent to call `cybe_guards_check` via MCP before sensitive actions — soft enforcement that relies on the model following its rules file.
|
|
50
|
+
|
|
51
|
+
² Windsurf has no native SessionStart event. We wire `pre_user_prompt`, which fires on every turn. The hook is idempotent and cheap (one GET to the proposals endpoint, returns "0 pending" once the inbox is empty), so the per-turn cost is negligible.
|
|
52
|
+
|
|
53
|
+
³ Codex follows the doctrine via the MCP server's `Server.instructions` field on each session, removing the need for a per-prompt reminder hook.
|
|
54
|
+
|
|
55
|
+
**Codex setup gotcha:** Codex 0.131+ requires you to approve each hook via the `/hooks` panel inside Codex *before* they fire. After running `vibedefend install`, open Codex, run `/hooks`, and trust the `cybedefend` entries — until you do that the panel will show `Installed N / Active 0`.
|
|
56
|
+
|
|
57
|
+
Unchecked agents stay untouched. Re-run `vibedefend install` later to
|
|
58
|
+
toggle any on or off.
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
vibedefend install Set up MCP + hooks (interactive)
|
|
64
|
+
vibedefend update Refresh hooks to the latest version
|
|
65
|
+
vibedefend update --self Upgrade the CLI itself
|
|
66
|
+
vibedefend status Read-only install report
|
|
67
|
+
vibedefend doctor Diagnose and repair common issues
|
|
68
|
+
vibedefend login (Re-)authenticate against the CybeDefend API
|
|
69
|
+
vibedefend uninstall Remove every VibeDefend-installed file
|
|
70
|
+
vibedefend --help Full help
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
After install you have one tiny file to drop in each repo you want
|
|
74
|
+
monitored — a `.cybedefend/config.json` with your project UUID:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{ "projectId": "<your-cybedefend-project-uuid>" }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
You can grab the UUID from the project page on the EU dashboard
|
|
81
|
+
([eu.cybedefend.com](https://eu.cybedefend.com)) or the US dashboard
|
|
82
|
+
([us.cybedefend.com](https://us.cybedefend.com)).
|
|
83
|
+
|
|
84
|
+
## Updating
|
|
85
|
+
|
|
86
|
+
Every time you run `vibedefend …`, a non-blocking check runs against npm
|
|
87
|
+
in the background. When a newer version is available you get a one-line
|
|
88
|
+
notice; run `vibedefend update --self` whenever you feel like it. Already-
|
|
89
|
+
installed hooks pull rule changes live, so most updates are silent.
|
|
90
|
+
|
|
91
|
+
## Uninstalling
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
vibedefend uninstall
|
|
95
|
+
# Then drop the global package if you installed it:
|
|
96
|
+
npm uninstall -g @cybedefend/vibedefend
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Sibling: `cybedefend-cli`
|
|
100
|
+
|
|
101
|
+
[`cybedefend-cli`](https://github.com/CybeDefend/cybedefend-cli) is the
|
|
102
|
+
platform CLI (scan, list vulnerabilities, manage projects from your
|
|
103
|
+
terminal or CI). VibeDefend handles the AI-agent integration side; the
|
|
104
|
+
two are complementary and unaware of each other.
|
|
105
|
+
|
|
106
|
+
## Documentation
|
|
107
|
+
|
|
108
|
+
Full documentation, configuration reference, and troubleshooting at
|
|
109
|
+
**[docs.cybedefend.com](https://docs.cybedefend.com)**.
|
|
110
|
+
|
|
111
|
+
## Support
|
|
112
|
+
|
|
113
|
+
- Bug reports / feature requests:
|
|
114
|
+
[support@cybedefend.com](mailto:support@cybedefend.com)
|
|
115
|
+
- Email: [support@cybedefend.com](mailto:support@cybedefend.com)
|
|
116
|
+
- Status: [status.cybedefend.com](https://status.cybedefend.com)
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
BUSL-1.1 — see [LICENSE](./LICENSE). Copyright 2026 CybeDefend. Converts to Apache-2.0 on 2030-05-25.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin shim — calls the compiled dist or runs from src in dev. Published
|
|
3
|
+
// builds ship `dist/index.js`; local `pnpm dev` uses tsx via the package's
|
|
4
|
+
// `dev` script.
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const distEntry = join(here, '..', 'dist', 'index.js');
|
|
11
|
+
|
|
12
|
+
if (existsSync(distEntry)) {
|
|
13
|
+
await import(distEntry);
|
|
14
|
+
} else {
|
|
15
|
+
console.error(
|
|
16
|
+
'vibedefend: dist/ not found. Run `pnpm run build` first, or use `pnpm dev` for source-mode.',
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent storage for the OAuth token bundle.
|
|
3
|
+
*
|
|
4
|
+
* Storage strategy (most secure first):
|
|
5
|
+
* macOS → macOS Keychain via `security` CLI (no install required).
|
|
6
|
+
* Linux → ~/.cybedefend/auth.json with mode 0600.
|
|
7
|
+
* Windows → ~/.cybedefend/auth.json with mode 0600 (best-effort; NTFS ACLs
|
|
8
|
+
* are not set by Node's writeFileSync, but it is an improvement
|
|
9
|
+
* over environment variables for interactive use).
|
|
10
|
+
*
|
|
11
|
+
* The entire StoredAuth object is JSON-serialised and stored as the keychain
|
|
12
|
+
* password blob, so all PKCE / OIDC state travels with it. On macOS the OS
|
|
13
|
+
* provides the encryption; on Linux the file permissions are the only control.
|
|
14
|
+
*
|
|
15
|
+
* Service name: "Codex MCP Credentials" — intentionally the same service that
|
|
16
|
+
* `resolveTokenCodexMacOS` reads in resolve.ts, so a Codex-style lookup
|
|
17
|
+
* (account = "<mcpName>|vibedefend") also picks up the new bundle and the
|
|
18
|
+
* existing `resolveToken` path still works without modification during
|
|
19
|
+
* the migration period.
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from 'node:child_process';
|
|
22
|
+
import { homedir } from 'node:os';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, } from 'node:fs';
|
|
25
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
26
|
+
const KEYCHAIN_SERVICE = 'Codex MCP Credentials';
|
|
27
|
+
/** Account shape: "<mcpName>|vibedefend" — matches resolveTokenCodexMacOS. */
|
|
28
|
+
function accountFor(mcpName) {
|
|
29
|
+
return `${mcpName}|vibedefend`;
|
|
30
|
+
}
|
|
31
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
32
|
+
/** Persist an auth bundle for the given MCP name. */
|
|
33
|
+
export function storeAuth(mcpName, auth) {
|
|
34
|
+
const blob = JSON.stringify(auth);
|
|
35
|
+
if (process.platform === 'darwin') {
|
|
36
|
+
storeMacOSKeychain(mcpName, blob);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
storeFileFallback(blob);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Load the stored auth bundle for the given MCP name.
|
|
44
|
+
* Returns null if nothing is stored or the blob is invalid.
|
|
45
|
+
*/
|
|
46
|
+
export function loadAuth(mcpName) {
|
|
47
|
+
let blob = null;
|
|
48
|
+
if (process.platform === 'darwin') {
|
|
49
|
+
blob = readMacOSKeychain(mcpName);
|
|
50
|
+
}
|
|
51
|
+
// File fallback — also checked on Linux/Windows and as fallback on macOS
|
|
52
|
+
// when the keychain entry is absent.
|
|
53
|
+
if (!blob)
|
|
54
|
+
blob = readFileFallback();
|
|
55
|
+
if (!blob)
|
|
56
|
+
return null;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(blob);
|
|
59
|
+
// refreshToken is OPTIONAL: Logto only emits one when the client app has
|
|
60
|
+
// the Refresh Token grant explicitly enabled AND the user requested
|
|
61
|
+
// offline_access. If absent, we can still operate — the user just has to
|
|
62
|
+
// `vibedefend login` again when access_token expires (no transparent refresh).
|
|
63
|
+
if (typeof parsed.accessToken === 'string' &&
|
|
64
|
+
typeof parsed.expiresAt === 'number' &&
|
|
65
|
+
typeof parsed.logtoEndpoint === 'string' &&
|
|
66
|
+
typeof parsed.cliAppId === 'string' &&
|
|
67
|
+
typeof parsed.logtoResource === 'string') {
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Remove stored credentials for the given MCP name. */
|
|
77
|
+
export function clearAuth(mcpName) {
|
|
78
|
+
if (process.platform === 'darwin') {
|
|
79
|
+
try {
|
|
80
|
+
execFileSync('security', [
|
|
81
|
+
'delete-generic-password',
|
|
82
|
+
'-s', KEYCHAIN_SERVICE,
|
|
83
|
+
'-a', accountFor(mcpName),
|
|
84
|
+
], { stdio: 'ignore' });
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Not found — that is fine.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Also clear the file fallback (it may hold an older copy on non-macOS).
|
|
91
|
+
const filePath = fileFallbackPath();
|
|
92
|
+
if (existsSync(filePath)) {
|
|
93
|
+
try {
|
|
94
|
+
writeFileSync(filePath, '', { mode: 0o600 });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Best-effort.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ─── macOS Keychain helpers ───────────────────────────────────────────────────
|
|
102
|
+
function storeMacOSKeychain(mcpName, blob) {
|
|
103
|
+
// Delete any existing entry first — `add-generic-password` fails if the
|
|
104
|
+
// account already exists.
|
|
105
|
+
try {
|
|
106
|
+
execFileSync('security', [
|
|
107
|
+
'delete-generic-password',
|
|
108
|
+
'-s', KEYCHAIN_SERVICE,
|
|
109
|
+
'-a', accountFor(mcpName),
|
|
110
|
+
], { stdio: 'ignore' });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Not found — that is fine.
|
|
114
|
+
}
|
|
115
|
+
execFileSync('security', [
|
|
116
|
+
'add-generic-password',
|
|
117
|
+
'-s', KEYCHAIN_SERVICE,
|
|
118
|
+
'-a', accountFor(mcpName),
|
|
119
|
+
'-w', blob,
|
|
120
|
+
], { stdio: 'ignore' });
|
|
121
|
+
}
|
|
122
|
+
function readMacOSKeychain(mcpName) {
|
|
123
|
+
try {
|
|
124
|
+
const out = execFileSync('security', [
|
|
125
|
+
'find-generic-password',
|
|
126
|
+
'-s', KEYCHAIN_SERVICE,
|
|
127
|
+
'-a', accountFor(mcpName),
|
|
128
|
+
'-w',
|
|
129
|
+
], {
|
|
130
|
+
encoding: 'utf8',
|
|
131
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
132
|
+
timeout: 3000,
|
|
133
|
+
});
|
|
134
|
+
return out.trim() || null;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ─── File fallback (Linux / Windows) ─────────────────────────────────────────
|
|
141
|
+
function fileFallbackPath() {
|
|
142
|
+
return join(homedir(), '.cybedefend', 'auth.json');
|
|
143
|
+
}
|
|
144
|
+
function storeFileFallback(blob) {
|
|
145
|
+
const dir = join(homedir(), '.cybedefend');
|
|
146
|
+
mkdirSync(dir, { recursive: true });
|
|
147
|
+
const p = fileFallbackPath();
|
|
148
|
+
writeFileSync(p, blob, { mode: 0o600 });
|
|
149
|
+
try {
|
|
150
|
+
// Belt-and-suspenders: explicitly chmod after write (some umask configs
|
|
151
|
+
// ignore the mode option on writeFileSync).
|
|
152
|
+
chmodSync(p, 0o600);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Non-fatal on Windows (no POSIX perms).
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function readFileFallback() {
|
|
159
|
+
const p = fileFallbackPath();
|
|
160
|
+
if (!existsSync(p))
|
|
161
|
+
return null;
|
|
162
|
+
try {
|
|
163
|
+
const content = readFileSync(p, 'utf8');
|
|
164
|
+
return content || null;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=auth-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-store.js","sourceRoot":"","sources":["../../src/auth/auth-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single entry point for auth state used by all hook callers.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Return a valid access_token, refreshing transparently when near expiry.
|
|
6
|
+
* - Deduplicate concurrent refresh calls (many hooks may fire simultaneously
|
|
7
|
+
* at session start; we must not hammer Logto with N refresh requests).
|
|
8
|
+
* - On refresh_token rejection, purge local credentials and throw
|
|
9
|
+
* LoginRequiredError so the caller can trigger the fail-open UNVERIFIED
|
|
10
|
+
* banner.
|
|
11
|
+
*
|
|
12
|
+
* This module is intentionally side-effect-free at import time — the
|
|
13
|
+
* in-flight deduplication state is module-level, but nothing fires until
|
|
14
|
+
* getValidAccessToken() is called.
|
|
15
|
+
*/
|
|
16
|
+
import { loadAuth, storeAuth, clearAuth, } from './auth-store.js';
|
|
17
|
+
import { refreshAccessToken, RefreshTokenInvalidError } from './token-exchange.js';
|
|
18
|
+
// ─── Errors ───────────────────────────────────────────────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when a valid access_token cannot be obtained because:
|
|
21
|
+
* - No credentials are stored at all, OR
|
|
22
|
+
* - The refresh_token has been rejected / revoked by Logto.
|
|
23
|
+
*
|
|
24
|
+
* Hook callers that catch this should fall back to the loud UNVERIFIED
|
|
25
|
+
* banner (fail-open semantics) so the dev session is never hard-blocked.
|
|
26
|
+
*/
|
|
27
|
+
export class LoginRequiredError extends Error {
|
|
28
|
+
constructor(msg = 'VibeDefend login required. Run `vibedefend login`.') {
|
|
29
|
+
super(msg);
|
|
30
|
+
this.name = 'LoginRequiredError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ─── In-flight deduplication ──────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* When multiple hooks fire in the same millisecond (session-start, guard-check,
|
|
36
|
+
* fetch-rules all launching concurrently) and the token is stale, they would all
|
|
37
|
+
* attempt a refresh simultaneously. We de-duplicate by keeping a single promise
|
|
38
|
+
* while a refresh is in flight and having every concurrent waiter share it.
|
|
39
|
+
*/
|
|
40
|
+
let inFlightRefresh = null;
|
|
41
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* Return a valid access_token for the given MCP name.
|
|
44
|
+
*
|
|
45
|
+
* - If the stored token is fresh (now < expiresAt), return it immediately.
|
|
46
|
+
* - If stale, initiate (or join an in-flight) refresh.
|
|
47
|
+
* - If the refresh_token is rejected, purge credentials and throw
|
|
48
|
+
* LoginRequiredError.
|
|
49
|
+
* - If no credentials are stored, throw LoginRequiredError immediately.
|
|
50
|
+
*
|
|
51
|
+
* Concurrent calls share a single in-flight refresh promise — Logto sees
|
|
52
|
+
* exactly one refresh request per expiry cycle.
|
|
53
|
+
*/
|
|
54
|
+
export async function getValidAccessToken(mcpName) {
|
|
55
|
+
const stored = loadAuth(mcpName);
|
|
56
|
+
if (!stored) {
|
|
57
|
+
throw new LoginRequiredError('No stored credentials. Run `vibedefend login`.');
|
|
58
|
+
}
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
if (now < stored.expiresAt) {
|
|
61
|
+
// Token is still valid.
|
|
62
|
+
return stored.accessToken;
|
|
63
|
+
}
|
|
64
|
+
// Token is stale. If we don't have a refresh_token (Logto didn't issue one
|
|
65
|
+
// because offline_access wasn't granted on this client), we cannot refresh
|
|
66
|
+
// silently — the user must `vibedefend login` again.
|
|
67
|
+
if (!stored.refreshToken) {
|
|
68
|
+
throw new LoginRequiredError('Access token expired and no refresh_token is available. Run `vibedefend login` again.');
|
|
69
|
+
}
|
|
70
|
+
// Token is stale — refresh (deduped).
|
|
71
|
+
if (!inFlightRefresh) {
|
|
72
|
+
inFlightRefresh = doRefresh(mcpName, stored).finally(() => {
|
|
73
|
+
inFlightRefresh = null;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return await inFlightRefresh;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Synchronous check: does the given MCP name have any stored credentials?
|
|
80
|
+
* Used by the install chain to skip the login prompt on re-install when
|
|
81
|
+
* credentials are already present.
|
|
82
|
+
*/
|
|
83
|
+
export function hasStoredAuth(mcpName) {
|
|
84
|
+
return loadAuth(mcpName) !== null;
|
|
85
|
+
}
|
|
86
|
+
// ─── Internal refresh logic ───────────────────────────────────────────────────
|
|
87
|
+
async function doRefresh(mcpName, stored) {
|
|
88
|
+
// Safety net: getValidAccessToken already checks this, but type-narrow here
|
|
89
|
+
// too so the call below doesn't pass undefined as a string.
|
|
90
|
+
if (!stored.refreshToken) {
|
|
91
|
+
throw new LoginRequiredError('No refresh_token available; re-login required.');
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const fresh = await refreshAccessToken({
|
|
95
|
+
logtoEndpoint: stored.logtoEndpoint,
|
|
96
|
+
clientId: stored.cliAppId,
|
|
97
|
+
refreshToken: stored.refreshToken,
|
|
98
|
+
resource: stored.logtoResource,
|
|
99
|
+
});
|
|
100
|
+
const updated = {
|
|
101
|
+
...stored,
|
|
102
|
+
accessToken: fresh.access_token,
|
|
103
|
+
// Logto issues a new refresh_token on every refresh (rolling tokens).
|
|
104
|
+
// Fall back to the existing one if the response omits it.
|
|
105
|
+
refreshToken: fresh.refresh_token ?? stored.refreshToken,
|
|
106
|
+
idToken: fresh.id_token ?? stored.idToken,
|
|
107
|
+
expiresAt: Date.now() + (fresh.expires_in - 30) * 1000,
|
|
108
|
+
};
|
|
109
|
+
storeAuth(mcpName, updated);
|
|
110
|
+
return updated.accessToken;
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
if (e instanceof RefreshTokenInvalidError) {
|
|
114
|
+
// Refresh token revoked / expired — nuke local state so the next
|
|
115
|
+
// `vibedefend login` starts from a clean slate.
|
|
116
|
+
clearAuth(mcpName);
|
|
117
|
+
throw new LoginRequiredError('Your session has expired. Run `vibedefend login` to re-authenticate.');
|
|
118
|
+
}
|
|
119
|
+
// Transient (network, DNS, Logto down): re-throw so the caller can
|
|
120
|
+
// trigger the fail-open banner. Do NOT clear credentials — the user
|
|
121
|
+
// should not be forced to log in again for a transient failure.
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/auth/auth.ts"],"names":[],"mappings":"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"}
|