borgmcp 0.9.57 → 0.9.58
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/README.md +104 -176
- package/dist/auth-env.d.ts +52 -0
- package/dist/auth-env.d.ts.map +1 -0
- package/dist/auth-env.js +107 -0
- package/dist/auth-env.js.map +1 -0
- package/dist/auth.d.ts +33 -13
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +100 -4
- package/dist/auth.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +6 -3
- package/dist/cli-help.js.map +1 -1
- package/dist/cli-platform.js +1 -1
- package/dist/cli-platform.js.map +1 -1
- package/dist/config.d.ts +13 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +105 -60
- package/dist/config.js.map +1 -1
- package/dist/device-auth.d.ts +75 -0
- package/dist/device-auth.d.ts.map +1 -0
- package/dist/device-auth.js +167 -0
- package/dist/device-auth.js.map +1 -0
- package/dist/setup.js +10 -1
- package/dist/setup.js.map +1 -1
- package/dist/token-crypto.d.ts +50 -0
- package/dist/token-crypto.d.ts.map +1 -0
- package/dist/token-crypto.js +91 -0
- package/dist/token-crypto.js.map +1 -0
- package/dist/token-store.d.ts +98 -0
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +136 -0
- package/dist/token-store.js.map +1 -0
- package/package.json +1 -9
package/README.md
CHANGED
|
@@ -1,250 +1,178 @@
|
|
|
1
|
-
# Borg MCP
|
|
1
|
+
# Borg MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Multi-agent coordination for AI coding agents.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Borg MCP lets Claude Code and Codex sessions join the same project coordination space, take on named roles, and coordinate through one shared activity log. That shared space is a **cube**. Each running agent session is a **drone**. Drones operate under **roles** such as Builder, Code Reviewer, Coordinator, UX Expert, or Security Auditor.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Conflict Detection**: Never lose data - conflicts are detected and backed up for manual resolution
|
|
9
|
-
- **Secure Storage**: OAuth tokens stored in OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service)
|
|
10
|
-
- **Offline Support**: Exponential backoff retry logic handles network failures gracefully
|
|
11
|
-
- **Google OAuth**: Secure authentication via Google OAuth 2.0 device code flow
|
|
7
|
+
## What you get
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
- Shared cube context: cube directive, role playbooks, roster, and recent log entries.
|
|
10
|
+
- Role-based coordination: assign drones to roles and keep their instructions in one place.
|
|
11
|
+
- Live activity log: post status, dispatches, review gates, and findings without copy-paste handoffs.
|
|
12
|
+
- Wake-path support: inbox monitoring and stream diagnostics help keep agent sessions responsive.
|
|
13
|
+
- Claude Code and Codex launcher support: start one or more agent sessions from the same repo.
|
|
14
|
+
- Dashboard support: manage cubes, roles, drones, and logs at <https://borgmcp.ai/dashboard>.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
2. **Google OAuth Credentials** (required for authentication)
|
|
17
|
-
- Client ID
|
|
18
|
-
- Client Secret
|
|
19
|
-
3. **Borg MCP Subscription** ($1/month with 7-day trial)
|
|
16
|
+
## Pricing
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
- Free tier: 1 cube, 3 drones, 100 requests/hour.
|
|
19
|
+
- Cube tier: $1/cube/month, unlimited cubes, unlimited drones, 1000 requests/hour.
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
Free tier is permanent. No trial is required.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
|
|
27
|
-
npm install
|
|
26
|
+
npm install -g borgmcp
|
|
28
27
|
```
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
Verify the install:
|
|
31
30
|
|
|
32
31
|
```bash
|
|
33
|
-
|
|
32
|
+
borg --version
|
|
33
|
+
borg --help
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
## First-time setup
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Run the setup wizard:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
|
|
42
|
-
export GOOGLE_CLIENT_SECRET="your-client-secret"
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 4. Configure Claude Code MCP Settings
|
|
46
|
-
|
|
47
|
-
Add the client to your Claude Code MCP configuration file at `~/.config/claude/mcp_config.json`:
|
|
48
|
-
|
|
49
|
-
```json
|
|
50
|
-
{
|
|
51
|
-
"mcpServers": {
|
|
52
|
-
"borg-mcp": {
|
|
53
|
-
"command": "node",
|
|
54
|
-
"args": [
|
|
55
|
-
"/absolute/path/to/mcp-claude-md-storage/client/dist/index.js"
|
|
56
|
-
],
|
|
57
|
-
"env": {
|
|
58
|
-
"GOOGLE_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
|
|
59
|
-
"GOOGLE_CLIENT_SECRET": "your-client-secret"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
41
|
+
borg setup
|
|
64
42
|
```
|
|
65
43
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
## First-Time Setup
|
|
69
|
-
|
|
70
|
-
### Authentication Flow
|
|
44
|
+
The wizard signs you in with Google, checks your subscription access, and registers Borg MCP with the supported agent CLIs installed on your machine.
|
|
71
45
|
|
|
72
|
-
|
|
46
|
+
If both Claude Code and Codex are installed, use `--cli` when launching or assimilating if you want to choose explicitly:
|
|
73
47
|
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
📱 Please visit: https://www.google.com/device
|
|
79
|
-
🔑 And enter code: XXXX-XXXX
|
|
80
|
-
|
|
81
|
-
Waiting for authorization...
|
|
48
|
+
```bash
|
|
49
|
+
borg assimilate --cli claude
|
|
50
|
+
borg assimilate --cli codex
|
|
82
51
|
```
|
|
83
52
|
|
|
84
|
-
|
|
85
|
-
2. Enter the device code
|
|
86
|
-
3. Authorize with your Google account
|
|
87
|
-
4. Tokens are securely stored in your OS keychain
|
|
53
|
+
## Join a cube
|
|
88
54
|
|
|
89
|
-
|
|
55
|
+
From inside your project repo:
|
|
90
56
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
Tools available:
|
|
95
|
-
- subscribe: Create Stripe checkout session ($2/month, 7-day trial)
|
|
57
|
+
```bash
|
|
58
|
+
borg assimilate
|
|
96
59
|
```
|
|
97
60
|
|
|
98
|
-
|
|
61
|
+
Borg derives a cube name from the repo, creates or joins that cube, registers the current session as a drone, and launches the selected agent CLI with cube context.
|
|
99
62
|
|
|
100
|
-
|
|
63
|
+
To start another drone in a sibling worktree:
|
|
101
64
|
|
|
102
|
-
|
|
65
|
+
```bash
|
|
66
|
+
borg assimilate builder --worktree drone-2
|
|
67
|
+
```
|
|
103
68
|
|
|
104
|
-
|
|
69
|
+
To join under a specific role:
|
|
105
70
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- If both exist and differ → **conflict detection**
|
|
71
|
+
```bash
|
|
72
|
+
borg assimilate code-reviewer --cli codex
|
|
73
|
+
```
|
|
110
74
|
|
|
111
|
-
|
|
75
|
+
## Core MCP tools
|
|
112
76
|
|
|
113
|
-
|
|
77
|
+
After assimilation, the agent session has `borg:` tools available:
|
|
114
78
|
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
79
|
+
- `borg:regen` - Refresh cube context, role instructions, roster, and recent log.
|
|
80
|
+
- `borg:log` - Append to the shared activity log. Can broadcast or direct messages to drones/roles.
|
|
81
|
+
- `borg:read-log` - Read recent log entries, optionally since an entry id or timestamp.
|
|
82
|
+
- `borg:ack` - Acknowledge a routed log entry without adding noise to the activity log.
|
|
83
|
+
- `borg:roster` - List drones and liveness markers in the cube.
|
|
84
|
+
- `borg:stream-status` - Diagnose the SSE/inbox wake path.
|
|
85
|
+
- `borg:cube`, `borg:role`, `borg:whoami` - Inspect current cube, role, and identity.
|
|
86
|
+
- `borg:create-cube`, `borg:update-cube`, `borg:delete-cube` - Manage cubes.
|
|
87
|
+
- `borg:create-role`, `borg:update-role`, `borg:reassign-drone` - Manage roles and drone assignments.
|
|
88
|
+
- `borg:apply-template`, `borg:sync-roles`, `borg:patch-taxonomy-class` - Bootstrap and maintain role/message-taxonomy templates.
|
|
118
89
|
|
|
119
|
-
|
|
90
|
+
## Typical two-agent flow
|
|
120
91
|
|
|
121
|
-
|
|
92
|
+
1. Install and run setup.
|
|
122
93
|
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
|
|
94
|
+
```bash
|
|
95
|
+
npm install -g borgmcp
|
|
96
|
+
borg setup
|
|
97
|
+
```
|
|
126
98
|
|
|
127
|
-
|
|
128
|
-
Local file: ~/.claude/CLAUDE.md
|
|
129
|
-
Remote backup: ~/.claude/CLAUDE.md.conflict
|
|
99
|
+
2. Open a repo and assimilate the first drone.
|
|
130
100
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
101
|
+
```bash
|
|
102
|
+
cd ~/code/my-app
|
|
103
|
+
borg assimilate --cli claude
|
|
104
|
+
```
|
|
134
105
|
|
|
135
|
-
|
|
136
|
-
1. Compare `~/.claude/CLAUDE.md` (local) with `~/.claude/CLAUDE.md.conflict` (remote)
|
|
137
|
-
2. Manually merge or choose one version
|
|
138
|
-
3. Delete `.conflict` file when done
|
|
106
|
+
3. Open a second terminal in the same repo and assimilate another drone.
|
|
139
107
|
|
|
140
|
-
|
|
108
|
+
```bash
|
|
109
|
+
cd ~/code/my-app
|
|
110
|
+
borg assimilate code-reviewer --cli codex
|
|
111
|
+
```
|
|
141
112
|
|
|
142
|
-
|
|
113
|
+
4. In the agent session, refresh context and coordinate through the log.
|
|
143
114
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
- **export_data**: Export all your data (GDPR compliance)
|
|
150
|
-
- **sync_now**: Force immediate sync with cloud
|
|
115
|
+
```text
|
|
116
|
+
borg:regen
|
|
117
|
+
borg:roster
|
|
118
|
+
borg:log "STARTING: review feat/login"
|
|
119
|
+
```
|
|
151
120
|
|
|
152
121
|
## Troubleshooting
|
|
153
122
|
|
|
154
|
-
###
|
|
123
|
+
### Authentication expired
|
|
155
124
|
|
|
156
|
-
|
|
125
|
+
Run setup again:
|
|
157
126
|
|
|
158
127
|
```bash
|
|
159
|
-
|
|
160
|
-
node dist/index.js
|
|
128
|
+
borg setup
|
|
161
129
|
```
|
|
162
130
|
|
|
163
|
-
###
|
|
131
|
+
### Setup says subscription was not found yet
|
|
164
132
|
|
|
165
|
-
|
|
166
|
-
2. Manually merge changes between local and conflict file
|
|
167
|
-
3. Delete `.conflict` file to clear the conflict state
|
|
133
|
+
If you just subscribed, activation can take a moment. The setup flow retries automatically and offers a recheck option.
|
|
168
134
|
|
|
169
|
-
###
|
|
135
|
+
### Both Claude Code and Codex are installed
|
|
170
136
|
|
|
171
|
-
|
|
172
|
-
- Check file permissions (must be readable/writable)
|
|
173
|
-
- Some editors use atomic writes (rename temp file) which may delay detection
|
|
137
|
+
Choose one:
|
|
174
138
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
- Max retries: 3
|
|
182
|
-
|
|
183
|
-
If network is down, edits are queued and will sync once connection is restored.
|
|
184
|
-
|
|
185
|
-
## Security
|
|
139
|
+
```bash
|
|
140
|
+
borg --cli claude
|
|
141
|
+
borg --cli codex
|
|
142
|
+
borg assimilate --cli claude
|
|
143
|
+
borg assimilate --cli codex
|
|
144
|
+
```
|
|
186
145
|
|
|
187
|
-
###
|
|
146
|
+
### Not connected to a cube
|
|
188
147
|
|
|
189
|
-
|
|
148
|
+
Run assimilation from your project repo:
|
|
190
149
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
150
|
+
```bash
|
|
151
|
+
borg assimilate
|
|
152
|
+
```
|
|
194
153
|
|
|
195
|
-
|
|
154
|
+
Then call `borg:regen` in the agent session.
|
|
196
155
|
|
|
197
|
-
###
|
|
156
|
+
### Wake path warning
|
|
198
157
|
|
|
199
|
-
-
|
|
200
|
-
- ID tokens are automatically refreshed when expired
|
|
201
|
-
- Auth tokens are automatically injected into all remote API calls
|
|
158
|
+
If `borg:regen` or `borg:stream-status` reports a broken wake path, arm the inbox monitor command it prints. The monitor is what wakes the agent session when another drone posts to the cube.
|
|
202
159
|
|
|
203
160
|
## Development
|
|
204
161
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
```bash
|
|
208
|
-
npm run dev
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### Building
|
|
212
|
-
|
|
213
|
-
```bash
|
|
214
|
-
npm run build
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Testing
|
|
162
|
+
From the repository root:
|
|
218
163
|
|
|
219
164
|
```bash
|
|
220
165
|
npm test
|
|
166
|
+
cd client && npx tsc --noEmit
|
|
221
167
|
```
|
|
222
168
|
|
|
223
|
-
|
|
169
|
+
Build the CLI package:
|
|
224
170
|
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
│ Claude Code │
|
|
228
|
-
└────────┬────────┘
|
|
229
|
-
│ stdio MCP
|
|
230
|
-
│
|
|
231
|
-
┌────────▼────────┐
|
|
232
|
-
│ Borg MCP │◄──┐
|
|
233
|
-
│ Client │ │ File Watcher
|
|
234
|
-
└────────┬────────┘ │ (chokidar)
|
|
235
|
-
│ │
|
|
236
|
-
│ ┌──▼──────────────┐
|
|
237
|
-
│ │ ~/.claude/ │
|
|
238
|
-
│ │ CLAUDE.md │
|
|
239
|
-
│ └─────────────────┘
|
|
240
|
-
│ HTTPS + Auth
|
|
241
|
-
│
|
|
242
|
-
┌────────▼────────┐
|
|
243
|
-
│ api.borgmcp.ai │
|
|
244
|
-
│ (Remote Server) │
|
|
245
|
-
└─────────────────┘
|
|
171
|
+
```bash
|
|
172
|
+
npm run build:cli
|
|
246
173
|
```
|
|
247
174
|
|
|
248
|
-
##
|
|
175
|
+
## Links
|
|
249
176
|
|
|
250
|
-
|
|
177
|
+
- Product site: <https://borgmcp.ai>
|
|
178
|
+
- Dashboard: <https://borgmcp.ai/dashboard>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh#557 — environment / capability detection for remote-terminal auth.
|
|
3
|
+
*
|
|
4
|
+
* The default OAuth path (auth.ts) opens a browser and listens on a
|
|
5
|
+
* localhost-loopback callback server. Both assumptions break on a remote
|
|
6
|
+
* terminal: there's no browser to open, and the loopback URL Google would
|
|
7
|
+
* redirect to is unreachable from the user's actual browser on another
|
|
8
|
+
* machine. These pure helpers decide when to fall back to the RFC 8628
|
|
9
|
+
* device-grant flow (no browser) and when the OS keychain is unavailable
|
|
10
|
+
* (so a keychain-less token store must be used instead).
|
|
11
|
+
*
|
|
12
|
+
* Kept free of side effects (env + platform are injected) so the decision
|
|
13
|
+
* logic is unit-testable without a real display / SSH session / keychain.
|
|
14
|
+
*/
|
|
15
|
+
export interface BrowserEnvProbe {
|
|
16
|
+
/** A snapshot of the relevant environment variables (defaults to process.env). */
|
|
17
|
+
env: NodeJS.ProcessEnv;
|
|
18
|
+
/** The platform string (defaults to process.platform). */
|
|
19
|
+
platform: NodeJS.Platform;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Decide whether the current environment lacks a usable local browser, so
|
|
23
|
+
* the loopback OAuth flow can't work and the device-grant flow should be
|
|
24
|
+
* used instead.
|
|
25
|
+
*
|
|
26
|
+
* Decision order (first match wins):
|
|
27
|
+
* 1. BORG_FORCE_BROWSER on → false (escape hatch: operator has an
|
|
28
|
+
* SSH-forwarded / X-forwarded browser and wants the loopback flow)
|
|
29
|
+
* 2. BORG_NO_BROWSER on → true (explicit opt-in to device flow)
|
|
30
|
+
* 3. SSH session → true (remote terminal — even on a Mac the
|
|
31
|
+
* browser that opens is on the *server*, unreachable to the user)
|
|
32
|
+
* 4. container marker → true (headless by construction)
|
|
33
|
+
* 5. Linux without any display (no DISPLAY, no WAYLAND_DISPLAY) → true
|
|
34
|
+
* 6. otherwise → false (desktop macOS/Windows, or Linux with
|
|
35
|
+
* a display — the loopback flow works)
|
|
36
|
+
*
|
|
37
|
+
* The `--no-browser` / `--device` CLI flag is surfaced by the caller as
|
|
38
|
+
* BORG_NO_BROWSER (or by passing an env with it set) so there's a single
|
|
39
|
+
* decision funnel.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isNoBrowserEnv(probe?: Partial<BrowserEnvProbe>): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Returns true when the OS keychain can be written to and read from. The
|
|
44
|
+
* round-trip is injectable so callers/tests can supply a deterministic
|
|
45
|
+
* probe; in production the default probe touches the real keychain.
|
|
46
|
+
*
|
|
47
|
+
* Any thrown error from the probe (no Secret Service, locked keychain,
|
|
48
|
+
* permission denial) is treated as "unavailable" — the caller then selects
|
|
49
|
+
* the encrypted-file fallback store.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isKeyringAvailable(roundTrip?: () => Promise<void>): Promise<boolean>;
|
|
52
|
+
//# sourceMappingURL=auth-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-env.d.ts","sourceRoot":"","sources":["../src/auth-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,WAAW,eAAe;IAC9B,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC3B;AAaD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAsBxE;AAsBD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,GAAE,MAAM,OAAO,CAAC,IAAI,CAA2B,GACvD,OAAO,CAAC,OAAO,CAAC,CAOlB"}
|
package/dist/auth-env.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh#557 — environment / capability detection for remote-terminal auth.
|
|
3
|
+
*
|
|
4
|
+
* The default OAuth path (auth.ts) opens a browser and listens on a
|
|
5
|
+
* localhost-loopback callback server. Both assumptions break on a remote
|
|
6
|
+
* terminal: there's no browser to open, and the loopback URL Google would
|
|
7
|
+
* redirect to is unreachable from the user's actual browser on another
|
|
8
|
+
* machine. These pure helpers decide when to fall back to the RFC 8628
|
|
9
|
+
* device-grant flow (no browser) and when the OS keychain is unavailable
|
|
10
|
+
* (so a keychain-less token store must be used instead).
|
|
11
|
+
*
|
|
12
|
+
* Kept free of side effects (env + platform are injected) so the decision
|
|
13
|
+
* logic is unit-testable without a real display / SSH session / keychain.
|
|
14
|
+
*/
|
|
15
|
+
import { AsyncEntry } from '@napi-rs/keyring';
|
|
16
|
+
/**
|
|
17
|
+
* A BORG_* toggle is "on" only when present and not one of the falsy
|
|
18
|
+
* spellings. Mirrors how the rest of the client reads boolean env vars:
|
|
19
|
+
* an unset var and the explicit "0"/"false"/"" spellings are all off.
|
|
20
|
+
*/
|
|
21
|
+
function envToggleOn(value) {
|
|
22
|
+
if (value === undefined)
|
|
23
|
+
return false;
|
|
24
|
+
const v = value.trim().toLowerCase();
|
|
25
|
+
return v !== '' && v !== '0' && v !== 'false' && v !== 'no';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Decide whether the current environment lacks a usable local browser, so
|
|
29
|
+
* the loopback OAuth flow can't work and the device-grant flow should be
|
|
30
|
+
* used instead.
|
|
31
|
+
*
|
|
32
|
+
* Decision order (first match wins):
|
|
33
|
+
* 1. BORG_FORCE_BROWSER on → false (escape hatch: operator has an
|
|
34
|
+
* SSH-forwarded / X-forwarded browser and wants the loopback flow)
|
|
35
|
+
* 2. BORG_NO_BROWSER on → true (explicit opt-in to device flow)
|
|
36
|
+
* 3. SSH session → true (remote terminal — even on a Mac the
|
|
37
|
+
* browser that opens is on the *server*, unreachable to the user)
|
|
38
|
+
* 4. container marker → true (headless by construction)
|
|
39
|
+
* 5. Linux without any display (no DISPLAY, no WAYLAND_DISPLAY) → true
|
|
40
|
+
* 6. otherwise → false (desktop macOS/Windows, or Linux with
|
|
41
|
+
* a display — the loopback flow works)
|
|
42
|
+
*
|
|
43
|
+
* The `--no-browser` / `--device` CLI flag is surfaced by the caller as
|
|
44
|
+
* BORG_NO_BROWSER (or by passing an env with it set) so there's a single
|
|
45
|
+
* decision funnel.
|
|
46
|
+
*/
|
|
47
|
+
export function isNoBrowserEnv(probe) {
|
|
48
|
+
const env = probe?.env ?? process.env;
|
|
49
|
+
const platform = probe?.platform ?? process.platform;
|
|
50
|
+
// 1. Explicit force-browser escape hatch wins over every no-browser signal.
|
|
51
|
+
if (envToggleOn(env.BORG_FORCE_BROWSER))
|
|
52
|
+
return false;
|
|
53
|
+
// 2. Explicit opt-in to the no-browser/device flow.
|
|
54
|
+
if (envToggleOn(env.BORG_NO_BROWSER))
|
|
55
|
+
return true;
|
|
56
|
+
// 3. SSH session — the terminal is remote, so any browser we open is on
|
|
57
|
+
// the far end and the loopback redirect is unreachable to the user.
|
|
58
|
+
if (env.SSH_TTY || env.SSH_CONNECTION || env.SSH_CLIENT)
|
|
59
|
+
return true;
|
|
60
|
+
// 4. Container (systemd/Docker/podman set `container=`); headless by build.
|
|
61
|
+
if (env.container)
|
|
62
|
+
return true;
|
|
63
|
+
// 5. Headless Linux: no X11 and no Wayland display = no browser to open.
|
|
64
|
+
if (platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY)
|
|
65
|
+
return true;
|
|
66
|
+
// 6. Desktop (macOS/Windows) or Linux-with-display: loopback flow is fine.
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The probe round-trip used to decide whether the OS keychain is usable.
|
|
71
|
+
* Performs a real set/get/delete against a throwaway account so a missing
|
|
72
|
+
* Secret Service (headless Linux), a locked keychain, or any other backend
|
|
73
|
+
* failure surfaces as a thrown error rather than a later mid-auth crash.
|
|
74
|
+
*/
|
|
75
|
+
async function defaultKeyringRoundTrip() {
|
|
76
|
+
const PROBE_SERVICE = 'borg-mcp';
|
|
77
|
+
const PROBE_ACCOUNT = '__borg_keyring_probe__';
|
|
78
|
+
const entry = new AsyncEntry(PROBE_SERVICE, PROBE_ACCOUNT);
|
|
79
|
+
await entry.setPassword('probe');
|
|
80
|
+
await entry.getPassword();
|
|
81
|
+
// Best-effort cleanup; a failed delete still proves the keychain works.
|
|
82
|
+
try {
|
|
83
|
+
await entry.deletePassword();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* leave the probe entry — its presence is harmless */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns true when the OS keychain can be written to and read from. The
|
|
91
|
+
* round-trip is injectable so callers/tests can supply a deterministic
|
|
92
|
+
* probe; in production the default probe touches the real keychain.
|
|
93
|
+
*
|
|
94
|
+
* Any thrown error from the probe (no Secret Service, locked keychain,
|
|
95
|
+
* permission denial) is treated as "unavailable" — the caller then selects
|
|
96
|
+
* the encrypted-file fallback store.
|
|
97
|
+
*/
|
|
98
|
+
export async function isKeyringAvailable(roundTrip = defaultKeyringRoundTrip) {
|
|
99
|
+
try {
|
|
100
|
+
await roundTrip();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=auth-env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-env.js","sourceRoot":"","sources":["../src/auth-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAS9C;;;;GAIG;AACH,SAAS,WAAW,CAAC,KAAyB;IAC5C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,IAAI,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,cAAc,CAAC,KAAgC;IAC7D,MAAM,GAAG,GAAG,KAAK,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACtC,MAAM,QAAQ,GAAG,KAAK,EAAE,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IAErD,4EAA4E;IAC5E,IAAI,WAAW,CAAC,GAAG,CAAC,kBAAkB,CAAC;QAAE,OAAO,KAAK,CAAC;IAEtD,oDAAoD;IACpD,IAAI,WAAW,CAAC,GAAG,CAAC,eAAe,CAAC;QAAE,OAAO,IAAI,CAAC;IAElD,wEAAwE;IACxE,uEAAuE;IACvE,IAAI,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,cAAc,IAAI,GAAG,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAErE,4EAA4E;IAC5E,IAAI,GAAG,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAE/B,yEAAyE;IACzE,IAAI,QAAQ,KAAK,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,eAAe;QAAE,OAAO,IAAI,CAAC;IAE9E,2EAA2E;IAC3E,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,uBAAuB;IACpC,MAAM,aAAa,GAAG,UAAU,CAAC;IACjC,MAAM,aAAa,GAAG,wBAAwB,CAAC;IAC/C,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;IAC3D,MAAM,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,MAAM,KAAK,CAAC,WAAW,EAAE,CAAC;IAC1B,wEAAwE;IACxE,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,cAAc,EAAE,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,sDAAsD;IACxD,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,YAAiC,uBAAuB;IAExD,IAAI,CAAC;QACH,MAAM,SAAS,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
package/dist/auth.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* 6. Exchange code for tokens
|
|
12
12
|
* 7. Store tokens securely in OS keychain
|
|
13
13
|
*/
|
|
14
|
+
import { type DeviceAuthConfig, type DeviceAuthDeps } from './device-auth.js';
|
|
14
15
|
/**
|
|
15
16
|
* Refresh-token-revoked / expired — the user must re-run `borg setup`
|
|
16
17
|
* to recover. Anchored on Google's canonical signal: HTTP 400 + JSON
|
|
@@ -43,21 +44,40 @@ export declare class RefreshTransientError extends Error {
|
|
|
43
44
|
constructor(message: string);
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* Decide whether to use the no-browser device-grant flow. An explicit
|
|
48
|
+
* `--no-browser`/`--device` (surfaced as opts.noBrowser) wins; otherwise
|
|
49
|
+
* auto-detect via isNoBrowserEnv (SSH session, container, headless Linux).
|
|
50
|
+
*/
|
|
51
|
+
export declare function shouldUseDeviceFlow(opts?: {
|
|
52
|
+
noBrowser?: boolean;
|
|
53
|
+
}): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Assemble the device-grant OAuth config from the environment. Enforces the
|
|
56
|
+
* gh#557 ESCALATION-1 gate: a "TVs & Limited Input devices" client id must be
|
|
57
|
+
* available (baked-in once the operator creates it, or via GOOGLE_DEVICE_CLIENT_ID).
|
|
58
|
+
* Without one we fail with an actionable error instead of hitting Google with
|
|
59
|
+
* the Desktop client, which rejects /device/code as invalid_client.
|
|
60
|
+
*/
|
|
61
|
+
export declare function buildDeviceAuthConfig(env?: NodeJS.ProcessEnv): DeviceAuthConfig;
|
|
62
|
+
/**
|
|
63
|
+
* The RFC 8628 device-grant flow (no browser). Prints a verification URL +
|
|
64
|
+
* user_code for the human to open on ANY device, polls Google until they
|
|
65
|
+
* authorize, then stores tokens in the selected backend. Network deps are
|
|
66
|
+
* injectable for tests; the device-poll state machine itself lives in
|
|
67
|
+
* device-auth.ts (fully unit-tested).
|
|
68
|
+
*/
|
|
69
|
+
export declare function authenticateWithDeviceFlow(deps?: DeviceAuthDeps, env?: NodeJS.ProcessEnv): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Perform the complete OAuth flow, choosing the browser/loopback flow or the
|
|
72
|
+
* no-browser device-grant flow based on the environment (gh#557). Stores
|
|
73
|
+
* tokens in the selected backend on success.
|
|
49
74
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* `prompt=consent select_account` (multi-value prompt) to force both the
|
|
53
|
-
* consent screen and account picker. Together, these clear Google's
|
|
54
|
-
* session-side memory of prior consent, ensuring Google issues a fresh
|
|
55
|
-
* `refresh_token` rather than deduping the consent and returning only an
|
|
56
|
-
* id_token. (Without this, Google's dedup behavior can leave the user with
|
|
57
|
-
* an id_token but no refresh_token, forcing manual re-setup after the ~1h
|
|
58
|
-
* id_token TTL expires.)
|
|
75
|
+
* @param opts.noBrowser force the device flow (`--no-browser`/`--device`);
|
|
76
|
+
* when omitted, the environment is auto-detected.
|
|
59
77
|
*/
|
|
60
|
-
export declare function authenticateWithGoogle(
|
|
78
|
+
export declare function authenticateWithGoogle(opts?: {
|
|
79
|
+
noBrowser?: boolean;
|
|
80
|
+
}): Promise<void>;
|
|
61
81
|
/**
|
|
62
82
|
* Refresh ID token using refresh token (gh#34 hardened).
|
|
63
83
|
*
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH;;;;;;;;;;;GAWG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;aACrB,SAAS,EAAE,MAAM;aAAkB,gBAAgB,CAAC,EAAE,MAAM;gBAA5D,SAAS,EAAE,MAAM,EAAkB,gBAAgB,CAAC,EAAE,MAAM,YAAA;CAQzF;AAED;;;;;;;;;;GAUG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;;;GAWG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;aACrB,SAAS,EAAE,MAAM;aAAkB,gBAAgB,CAAC,EAAE,MAAM;gBAA5D,SAAS,EAAE,MAAM,EAAkB,gBAAgB,CAAC,EAAE,MAAM,YAAA;CAQzF;AAED;;;;;;;;;;GAUG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAmTD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAE3E;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAc5F;AAOD;;;;;;GAMG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,GAAE,cAA+C,EACrD,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,IAAI,CAAC,CAoCf;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAK1F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAsB,cAAc,CAClC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAiGf"}
|