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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh#557 — Google OAuth 2.0 Device Authorization Grant (RFC 8628).
|
|
3
|
+
*
|
|
4
|
+
* The no-browser counterpart to the loopback flow in auth.ts. Instead of
|
|
5
|
+
* opening a browser and listening on localhost, the device flow:
|
|
6
|
+
* 1. asks Google for a device_code + a short human-typable user_code,
|
|
7
|
+
* 2. prints a verification URL + the user_code for the human to open on
|
|
8
|
+
* ANY device (their phone, a laptop with a browser), and
|
|
9
|
+
* 3. polls Google's token endpoint until the human authorizes (or the
|
|
10
|
+
* code expires / is denied).
|
|
11
|
+
*
|
|
12
|
+
* This module is decoupled from the live Google client: `fetch`, `sleep`,
|
|
13
|
+
* and `now` are injected, and the client_id / client_secret / endpoints
|
|
14
|
+
* come from the caller. The live device flow needs a Google OAuth client
|
|
15
|
+
* of type "TVs & Limited Input devices" (a separate GOOGLE_DEVICE_CLIENT_ID
|
|
16
|
+
* — Desktop/loopback clients reject /device/code with invalid_client); the
|
|
17
|
+
* wiring layer supplies those credentials. Everything here is unit-tested
|
|
18
|
+
* against a mocked Google.
|
|
19
|
+
*/
|
|
20
|
+
const GOOGLE_DEVICE_CODE_URL = 'https://oauth2.googleapis.com/device/code';
|
|
21
|
+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
22
|
+
const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
23
|
+
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
24
|
+
const SLOW_DOWN_INCREMENT_SECONDS = 5;
|
|
25
|
+
/**
|
|
26
|
+
* Failure of the device-grant flow. `code` is Google's OAuth error code
|
|
27
|
+
* where one exists (`access_denied`, `expired_token`, `invalid_client`,
|
|
28
|
+
* `slow_down`, `authorization_pending`) or a synthetic code for
|
|
29
|
+
* transport/shape failures (`device_code_request_failed`,
|
|
30
|
+
* `device_token_request_failed`, `malformed_token_response`).
|
|
31
|
+
*
|
|
32
|
+
* Token material is never placed in the message — only Google's error
|
|
33
|
+
* code + description, mirroring RefreshTokenInvalidError's discipline.
|
|
34
|
+
*/
|
|
35
|
+
export class DeviceAuthError extends Error {
|
|
36
|
+
code;
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(message ?? code);
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.name = 'DeviceAuthError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Step 1 — request a device_code + user_code from Google.
|
|
45
|
+
*/
|
|
46
|
+
export async function requestDeviceCode(config, deps) {
|
|
47
|
+
const url = config.deviceCodeUrl ?? GOOGLE_DEVICE_CODE_URL;
|
|
48
|
+
let response;
|
|
49
|
+
try {
|
|
50
|
+
response = await deps.fetch(url, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
53
|
+
body: new URLSearchParams({
|
|
54
|
+
client_id: config.clientId,
|
|
55
|
+
scope: config.scopes.join(' '),
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new DeviceAuthError('device_code_request_failed', `Could not reach Google device endpoint: ${err?.message ?? 'unknown'}`);
|
|
61
|
+
}
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const code = await readErrorCode(response);
|
|
64
|
+
throw new DeviceAuthError(code ?? 'device_code_request_failed', `Device-code request failed (HTTP ${response.status}${code ? `, ${code}` : ''})`);
|
|
65
|
+
}
|
|
66
|
+
let data;
|
|
67
|
+
try {
|
|
68
|
+
data = (await response.json());
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new DeviceAuthError('malformed_token_response', 'Device-code response was not JSON');
|
|
72
|
+
}
|
|
73
|
+
if (!data.device_code || !data.user_code || !data.verification_url) {
|
|
74
|
+
throw new DeviceAuthError('malformed_token_response', 'Device-code response missing device_code/user_code/verification_url');
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
device_code: data.device_code,
|
|
78
|
+
user_code: data.user_code,
|
|
79
|
+
verification_url: data.verification_url,
|
|
80
|
+
expires_in: typeof data.expires_in === 'number' ? data.expires_in : 1800,
|
|
81
|
+
interval: typeof data.interval === 'number' ? data.interval : DEFAULT_INTERVAL_SECONDS,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Step 2 — poll Google's token endpoint until the user authorizes the
|
|
86
|
+
* device_code, honoring the RFC 8628 poll semantics.
|
|
87
|
+
*
|
|
88
|
+
* Sleeps `interval` BEFORE each poll (never hammers immediately). A local
|
|
89
|
+
* deadline derived from `expires_in` bounds the loop so a code the user
|
|
90
|
+
* abandons can't poll forever even if Google never returns expired_token.
|
|
91
|
+
*/
|
|
92
|
+
export async function pollForDeviceToken(deviceCode, config, deps) {
|
|
93
|
+
const tokenUrl = config.tokenUrl ?? GOOGLE_TOKEN_URL;
|
|
94
|
+
const now = deps.now ?? Date.now;
|
|
95
|
+
const deadline = now() + deviceCode.expires_in * 1000;
|
|
96
|
+
let intervalSeconds = deviceCode.interval > 0 ? deviceCode.interval : DEFAULT_INTERVAL_SECONDS;
|
|
97
|
+
// eslint-disable-next-line no-constant-condition
|
|
98
|
+
while (true) {
|
|
99
|
+
if (now() >= deadline) {
|
|
100
|
+
throw new DeviceAuthError('expired_token', 'Device code expired before the authorization was completed');
|
|
101
|
+
}
|
|
102
|
+
await deps.sleep(intervalSeconds * 1000);
|
|
103
|
+
let response;
|
|
104
|
+
try {
|
|
105
|
+
response = await deps.fetch(tokenUrl, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
108
|
+
body: new URLSearchParams({
|
|
109
|
+
client_id: config.clientId,
|
|
110
|
+
...(config.clientSecret ? { client_secret: config.clientSecret } : {}),
|
|
111
|
+
device_code: deviceCode.device_code,
|
|
112
|
+
grant_type: DEVICE_GRANT_TYPE,
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
throw new DeviceAuthError('device_token_request_failed', `Could not reach Google token endpoint: ${err?.message ?? 'unknown'}`);
|
|
118
|
+
}
|
|
119
|
+
if (response.ok) {
|
|
120
|
+
let data;
|
|
121
|
+
try {
|
|
122
|
+
data = (await response.json());
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new DeviceAuthError('malformed_token_response', 'Token response was not JSON');
|
|
126
|
+
}
|
|
127
|
+
if (!data.id_token || typeof data.expires_in !== 'number') {
|
|
128
|
+
throw new DeviceAuthError('malformed_token_response', 'Token response missing id_token or expires_in');
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
id_token: data.id_token,
|
|
132
|
+
refresh_token: data.refresh_token,
|
|
133
|
+
expires_in: data.expires_in,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const code = await readErrorCode(response);
|
|
137
|
+
switch (code) {
|
|
138
|
+
case 'authorization_pending':
|
|
139
|
+
// The user hasn't finished yet — keep polling at the same interval.
|
|
140
|
+
continue;
|
|
141
|
+
case 'slow_down':
|
|
142
|
+
// Google asks us to back off; RFC 8628 §3.5 → bump the interval.
|
|
143
|
+
intervalSeconds += SLOW_DOWN_INCREMENT_SECONDS;
|
|
144
|
+
continue;
|
|
145
|
+
case 'access_denied':
|
|
146
|
+
throw new DeviceAuthError('access_denied', 'Authorization was denied by the user');
|
|
147
|
+
case 'expired_token':
|
|
148
|
+
throw new DeviceAuthError('expired_token', 'Device code expired before authorization');
|
|
149
|
+
default:
|
|
150
|
+
throw new DeviceAuthError(code ?? 'device_token_request_failed', `Device token poll failed (HTTP ${response.status}${code ? `, ${code}` : ''})`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Extract Google's OAuth `error` code from a non-2xx response body without
|
|
156
|
+
* throwing. Returns null when the body isn't JSON or has no error field.
|
|
157
|
+
*/
|
|
158
|
+
async function readErrorCode(response) {
|
|
159
|
+
try {
|
|
160
|
+
const body = (await response.json());
|
|
161
|
+
return typeof body?.error === 'string' ? body.error : null;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=device-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-auth.js","sourceRoot":"","sources":["../src/device-auth.ts"],"names":[],"mappings":"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"}
|
package/dist/setup.js
CHANGED
|
@@ -23,6 +23,11 @@ import { handleVersionFlag } from './version.js';
|
|
|
23
23
|
async function main() {
|
|
24
24
|
handleVersionFlag();
|
|
25
25
|
console.log(chalk.blue.bold('\n◼ Borg MCP Setup Wizard ◼'));
|
|
26
|
+
// gh#557: `--no-browser` (alias `--device`) forces the device-code OAuth
|
|
27
|
+
// flow for SSH / headless / container terminals. Scanned from argv so it
|
|
28
|
+
// works both as `borg setup --no-browser` and `borg-setup --no-browser`.
|
|
29
|
+
// (SSH/headless are auto-detected too; the flag is the explicit override.)
|
|
30
|
+
const noBrowser = process.argv.includes('--no-browser') || process.argv.includes('--device');
|
|
26
31
|
// Step 0: Check which agent CLIs exist
|
|
27
32
|
let claudeCliPath = null;
|
|
28
33
|
let codexCliPath = null;
|
|
@@ -86,10 +91,14 @@ async function main() {
|
|
|
86
91
|
const authed = await isAuthenticated();
|
|
87
92
|
if (!authed) {
|
|
88
93
|
try {
|
|
89
|
-
await authenticateWithGoogle();
|
|
94
|
+
await authenticateWithGoogle(noBrowser ? { noBrowser: true } : undefined);
|
|
90
95
|
}
|
|
91
96
|
catch (error) {
|
|
92
97
|
console.error(chalk.red(`\n◼ Authentication failed: ${error.message}\n`));
|
|
98
|
+
// gh#557 NOTE-2: device-flow errors (access_denied / expired_token) and
|
|
99
|
+
// any other auth failure exit here — give the remote user a recovery
|
|
100
|
+
// path instead of a bare exit.
|
|
101
|
+
console.error(chalk.yellow('Re-run `borg setup` to try again.\n'));
|
|
93
102
|
process.exit(1);
|
|
94
103
|
}
|
|
95
104
|
}
|
package/dist/setup.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh#557 — AES-256-GCM crypto for the keychain-less token store.
|
|
3
|
+
*
|
|
4
|
+
* ⚠ OBFUSCATION-GRADE, BY DESIGN. The encryption key is DERIVED from stable
|
|
5
|
+
* machine+user identifiers (hostname, username, platform) — there is no
|
|
6
|
+
* passphrase. This means:
|
|
7
|
+
*
|
|
8
|
+
* - It DEFENDS against casual/accidental exposure: a dotfile backup, an
|
|
9
|
+
* `scp -r ~`, a synced home directory, a shoulder-surfed `cat`. The
|
|
10
|
+
* on-disk bytes are ciphertext, not a readable token.
|
|
11
|
+
* - It does NOT defend against a same-uid process or root on the SAME
|
|
12
|
+
* machine: anything that can read ~/.borg/credentials can also read the
|
|
13
|
+
* same hostname/username/platform and re-derive the key. That is an
|
|
14
|
+
* accepted limitation (SR-endorsed, gh#557 ESCALATION 2) and matches
|
|
15
|
+
* gcloud's own at-rest posture for its credential files.
|
|
16
|
+
*
|
|
17
|
+
* The OS keychain (config.ts default) remains the real at-rest encryption
|
|
18
|
+
* path; this fallback only engages when no keychain is available (headless
|
|
19
|
+
* Linux without Secret Service, etc.).
|
|
20
|
+
*
|
|
21
|
+
* Machine identifiers are injected (MachineKeyInputs) rather than read here,
|
|
22
|
+
* both for testability and so the production caller can choose OS primitives
|
|
23
|
+
* that work in headless/container environments (os.hostname()/os.userInfo()
|
|
24
|
+
* never spawn a subprocess, unlike a hardware machine-id probe).
|
|
25
|
+
*/
|
|
26
|
+
export interface MachineKeyInputs {
|
|
27
|
+
hostname: string;
|
|
28
|
+
username: string;
|
|
29
|
+
platform: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Derive a stable 32-byte AES-256 key from machine+user identifiers.
|
|
33
|
+
* Deterministic for a given machine+user (so a token written today decrypts
|
|
34
|
+
* tomorrow) and distinct across machines/users.
|
|
35
|
+
*/
|
|
36
|
+
export declare function deriveMachineKey(inputs: MachineKeyInputs): Buffer;
|
|
37
|
+
/**
|
|
38
|
+
* Encrypt a plaintext string under the given key. Returns a versioned,
|
|
39
|
+
* dot-delimited envelope: `v1.<base64(iv)>.<base64(tag)>.<base64(ct)>`.
|
|
40
|
+
* A fresh random IV per call means the same plaintext encrypts differently
|
|
41
|
+
* every time (no deterministic-ciphertext leak).
|
|
42
|
+
*/
|
|
43
|
+
export declare function encryptString(plaintext: string, key: Buffer): string;
|
|
44
|
+
/**
|
|
45
|
+
* Decrypt an envelope produced by encryptString. Throws on a malformed
|
|
46
|
+
* envelope, a wrong key, or a tampered ciphertext (the GCM auth tag fails
|
|
47
|
+
* verification) — fail-closed is correct for credential material.
|
|
48
|
+
*/
|
|
49
|
+
export declare function decryptString(envelope: string, key: Buffer): string;
|
|
50
|
+
//# sourceMappingURL=token-crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-crypto.d.ts","sourceRoot":"","sources":["../src/token-crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAeH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAGjE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAcpE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAkBnE"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh#557 — AES-256-GCM crypto for the keychain-less token store.
|
|
3
|
+
*
|
|
4
|
+
* ⚠ OBFUSCATION-GRADE, BY DESIGN. The encryption key is DERIVED from stable
|
|
5
|
+
* machine+user identifiers (hostname, username, platform) — there is no
|
|
6
|
+
* passphrase. This means:
|
|
7
|
+
*
|
|
8
|
+
* - It DEFENDS against casual/accidental exposure: a dotfile backup, an
|
|
9
|
+
* `scp -r ~`, a synced home directory, a shoulder-surfed `cat`. The
|
|
10
|
+
* on-disk bytes are ciphertext, not a readable token.
|
|
11
|
+
* - It does NOT defend against a same-uid process or root on the SAME
|
|
12
|
+
* machine: anything that can read ~/.borg/credentials can also read the
|
|
13
|
+
* same hostname/username/platform and re-derive the key. That is an
|
|
14
|
+
* accepted limitation (SR-endorsed, gh#557 ESCALATION 2) and matches
|
|
15
|
+
* gcloud's own at-rest posture for its credential files.
|
|
16
|
+
*
|
|
17
|
+
* The OS keychain (config.ts default) remains the real at-rest encryption
|
|
18
|
+
* path; this fallback only engages when no keychain is available (headless
|
|
19
|
+
* Linux without Secret Service, etc.).
|
|
20
|
+
*
|
|
21
|
+
* Machine identifiers are injected (MachineKeyInputs) rather than read here,
|
|
22
|
+
* both for testability and so the production caller can choose OS primitives
|
|
23
|
+
* that work in headless/container environments (os.hostname()/os.userInfo()
|
|
24
|
+
* never spawn a subprocess, unlike a hardware machine-id probe).
|
|
25
|
+
*/
|
|
26
|
+
import crypto from 'crypto';
|
|
27
|
+
/**
|
|
28
|
+
* Static application salt — domain-separates this key derivation from any
|
|
29
|
+
* other use of the same machine identifiers. NOT a secret (it ships in the
|
|
30
|
+
* published client); its only job is to make the derived key specific to
|
|
31
|
+
* borg-mcp token storage.
|
|
32
|
+
*/
|
|
33
|
+
const KEY_SALT = 'borg-mcp/token-store/v1';
|
|
34
|
+
const ENVELOPE_VERSION = 'v1';
|
|
35
|
+
const IV_BYTES = 12; // 96-bit nonce, the GCM standard
|
|
36
|
+
const KEY_BYTES = 32; // AES-256
|
|
37
|
+
/**
|
|
38
|
+
* Derive a stable 32-byte AES-256 key from machine+user identifiers.
|
|
39
|
+
* Deterministic for a given machine+user (so a token written today decrypts
|
|
40
|
+
* tomorrow) and distinct across machines/users.
|
|
41
|
+
*/
|
|
42
|
+
export function deriveMachineKey(inputs) {
|
|
43
|
+
const material = [inputs.hostname, inputs.username, inputs.platform, KEY_SALT].join('\0');
|
|
44
|
+
return crypto.createHash('sha256').update(material).digest(); // 32 bytes
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Encrypt a plaintext string under the given key. Returns a versioned,
|
|
48
|
+
* dot-delimited envelope: `v1.<base64(iv)>.<base64(tag)>.<base64(ct)>`.
|
|
49
|
+
* A fresh random IV per call means the same plaintext encrypts differently
|
|
50
|
+
* every time (no deterministic-ciphertext leak).
|
|
51
|
+
*/
|
|
52
|
+
export function encryptString(plaintext, key) {
|
|
53
|
+
if (key.length !== KEY_BYTES) {
|
|
54
|
+
throw new Error(`token-crypto: key must be ${KEY_BYTES} bytes`);
|
|
55
|
+
}
|
|
56
|
+
const iv = crypto.randomBytes(IV_BYTES);
|
|
57
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
58
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
59
|
+
const tag = cipher.getAuthTag();
|
|
60
|
+
return [
|
|
61
|
+
ENVELOPE_VERSION,
|
|
62
|
+
iv.toString('base64'),
|
|
63
|
+
tag.toString('base64'),
|
|
64
|
+
ciphertext.toString('base64'),
|
|
65
|
+
].join('.');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Decrypt an envelope produced by encryptString. Throws on a malformed
|
|
69
|
+
* envelope, a wrong key, or a tampered ciphertext (the GCM auth tag fails
|
|
70
|
+
* verification) — fail-closed is correct for credential material.
|
|
71
|
+
*/
|
|
72
|
+
export function decryptString(envelope, key) {
|
|
73
|
+
if (key.length !== KEY_BYTES) {
|
|
74
|
+
throw new Error(`token-crypto: key must be ${KEY_BYTES} bytes`);
|
|
75
|
+
}
|
|
76
|
+
const parts = envelope.split('.');
|
|
77
|
+
if (parts.length !== 4 || parts[0] !== ENVELOPE_VERSION) {
|
|
78
|
+
throw new Error('token-crypto: malformed or unsupported envelope');
|
|
79
|
+
}
|
|
80
|
+
const iv = Buffer.from(parts[1], 'base64');
|
|
81
|
+
const tag = Buffer.from(parts[2], 'base64');
|
|
82
|
+
const ciphertext = Buffer.from(parts[3], 'base64');
|
|
83
|
+
if (iv.length !== IV_BYTES) {
|
|
84
|
+
throw new Error('token-crypto: malformed IV');
|
|
85
|
+
}
|
|
86
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
87
|
+
decipher.setAuthTag(tag);
|
|
88
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
89
|
+
return plaintext.toString('utf8');
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=token-crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-crypto.js","sourceRoot":"","sources":["../src/token-crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh#557 — token storage backends + selection.
|
|
3
|
+
*
|
|
4
|
+
* config.ts exposes the public token API (storeIdToken/getIdToken/...). This
|
|
5
|
+
* module supplies the interchangeable storage engines it sits on top of:
|
|
6
|
+
*
|
|
7
|
+
* - KeychainBackend — OS keychain via @napi-rs/keyring (the default;
|
|
8
|
+
* real platform at-rest encryption).
|
|
9
|
+
* - EncryptedFileBackend — ~/.borg/credentials, all accounts in one
|
|
10
|
+
* AES-256-GCM blob, file 0600 / dir 0700. Engages
|
|
11
|
+
* only when no keychain is available. Obfuscation-
|
|
12
|
+
* grade (see token-crypto.ts).
|
|
13
|
+
* - caller-managed — BORG_TOKEN / BORG_TOKEN_FILE: an externally
|
|
14
|
+
* supplied id_token, used read-only with no store
|
|
15
|
+
* (the caller owns its lifecycle/freshness).
|
|
16
|
+
*
|
|
17
|
+
* Every engine takes its side-effecting dependencies (keyring entry factory,
|
|
18
|
+
* fs, machine key) by injection so the logic is unit-tested without a real
|
|
19
|
+
* keychain or disk.
|
|
20
|
+
*/
|
|
21
|
+
export type TokenBackendName = 'keychain' | 'encrypted-file';
|
|
22
|
+
/**
|
|
23
|
+
* Account-agnostic key/value store over a backing engine. `account` is one
|
|
24
|
+
* of config.ts's three slots (id-token, refresh-token, expiry).
|
|
25
|
+
*/
|
|
26
|
+
export interface TokenBackend {
|
|
27
|
+
readonly name: TokenBackendName;
|
|
28
|
+
get(account: string): Promise<string | null>;
|
|
29
|
+
set(account: string, value: string): Promise<void>;
|
|
30
|
+
delete(account: string): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The slice of @napi-rs/keyring's AsyncEntry this backend depends on. The
|
|
34
|
+
* return types mirror AsyncEntry exactly (deletePassword resolves to an
|
|
35
|
+
* implementation-defined value we ignore) so the real class is assignable.
|
|
36
|
+
*/
|
|
37
|
+
export interface KeyringEntry {
|
|
38
|
+
setPassword(value: string): Promise<void>;
|
|
39
|
+
getPassword(): Promise<string | null | undefined>;
|
|
40
|
+
deletePassword(): Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
export type KeyringEntryFactory = (account: string) => KeyringEntry;
|
|
43
|
+
/**
|
|
44
|
+
* Build the OS-keychain backend. Preserves config.ts's prior semantics:
|
|
45
|
+
* a missing entry reads as null, and delete is silent on a NoEntry error
|
|
46
|
+
* (idempotent clear) while other errors propagate (fail-loud).
|
|
47
|
+
*/
|
|
48
|
+
export declare function makeKeychainBackend(entryFactory?: KeyringEntryFactory): TokenBackend;
|
|
49
|
+
/** The minimal fs surface the file backend needs (injected for tests). */
|
|
50
|
+
export interface FileStoreFs {
|
|
51
|
+
readFile(filePath: string): Promise<string>;
|
|
52
|
+
writeFile(filePath: string, data: string, mode: number): Promise<void>;
|
|
53
|
+
mkdir(dir: string, mode: number): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
export interface EncryptedFileBackendDeps {
|
|
56
|
+
filePath: string;
|
|
57
|
+
key: Buffer;
|
|
58
|
+
fs: FileStoreFs;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build the encrypted-file backend. All accounts live in one JSON object
|
|
62
|
+
* encrypted as a single AES-256-GCM envelope at `filePath`.
|
|
63
|
+
*
|
|
64
|
+
* A missing file reads as an empty map. A file that won't decrypt (wrong
|
|
65
|
+
* machine key after a hostname change, truncation, tampering) is ALSO
|
|
66
|
+
* treated as empty: the only consequence is the user re-runs `borg setup`,
|
|
67
|
+
* which is the right fail-safe for credential material — a hard crash on a
|
|
68
|
+
* corrupt dotfile would be worse UX than transparent re-auth.
|
|
69
|
+
*/
|
|
70
|
+
export declare function makeEncryptedFileBackend(deps: EncryptedFileBackendDeps): TokenBackend;
|
|
71
|
+
/** User-facing BORG_TOKEN_STORE values (friendlier than the backend names). */
|
|
72
|
+
export type ForcedStore = 'keychain' | 'file';
|
|
73
|
+
export interface SelectTokenBackendDeps {
|
|
74
|
+
keyringAvailable: () => Promise<boolean>;
|
|
75
|
+
makeKeychain: () => TokenBackend;
|
|
76
|
+
makeFile: () => TokenBackend;
|
|
77
|
+
/** BORG_TOKEN_STORE override: skip probing and force a backend. */
|
|
78
|
+
forced?: ForcedStore;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Select the persistent backend: a forced choice (BORG_TOKEN_STORE=keychain|file)
|
|
82
|
+
* wins and skips the probe; otherwise probe the keychain and fall back to the
|
|
83
|
+
* encrypted file when it's unavailable.
|
|
84
|
+
*/
|
|
85
|
+
export declare function selectTokenBackend(deps: SelectTokenBackendDeps): Promise<TokenBackend>;
|
|
86
|
+
export interface CallerManagedDeps {
|
|
87
|
+
env: NodeJS.ProcessEnv;
|
|
88
|
+
readFile: (filePath: string) => Promise<string>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolve an externally-supplied id_token (no storage). BORG_TOKEN takes
|
|
92
|
+
* precedence; otherwise BORG_TOKEN_FILE is read from disk. Returns null when
|
|
93
|
+
* neither is configured. The value is trimmed (env vars and files commonly
|
|
94
|
+
* carry trailing newlines). The caller owns this token's freshness, so it
|
|
95
|
+
* bypasses the keychain AND the expiry check in config.ts.
|
|
96
|
+
*/
|
|
97
|
+
export declare function readCallerManagedIdToken(deps: CallerManagedDeps): Promise<string | null>;
|
|
98
|
+
//# sourceMappingURL=token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../src/token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAE7D;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAID;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAClD,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,YAAY,CAAC;AAKpE;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,GAAE,mBAAyC,GACtD,YAAY,CAmBd;AAID,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,WAAW,CAAC;CACjB;AAID;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,wBAAwB,GAAG,YAAY,CA2CrF;AAID,+EAA+E;AAC/E,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,MAAM,CAAC;AAE9C,MAAM,WAAW,sBAAsB;IACrC,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,YAAY,EAAE,MAAM,YAAY,CAAC;IACjC,QAAQ,EAAE,MAAM,YAAY,CAAC;IAC7B,mEAAmE;IACnE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAIvB;AAID,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACjD;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWxB"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh#557 — token storage backends + selection.
|
|
3
|
+
*
|
|
4
|
+
* config.ts exposes the public token API (storeIdToken/getIdToken/...). This
|
|
5
|
+
* module supplies the interchangeable storage engines it sits on top of:
|
|
6
|
+
*
|
|
7
|
+
* - KeychainBackend — OS keychain via @napi-rs/keyring (the default;
|
|
8
|
+
* real platform at-rest encryption).
|
|
9
|
+
* - EncryptedFileBackend — ~/.borg/credentials, all accounts in one
|
|
10
|
+
* AES-256-GCM blob, file 0600 / dir 0700. Engages
|
|
11
|
+
* only when no keychain is available. Obfuscation-
|
|
12
|
+
* grade (see token-crypto.ts).
|
|
13
|
+
* - caller-managed — BORG_TOKEN / BORG_TOKEN_FILE: an externally
|
|
14
|
+
* supplied id_token, used read-only with no store
|
|
15
|
+
* (the caller owns its lifecycle/freshness).
|
|
16
|
+
*
|
|
17
|
+
* Every engine takes its side-effecting dependencies (keyring entry factory,
|
|
18
|
+
* fs, machine key) by injection so the logic is unit-tested without a real
|
|
19
|
+
* keychain or disk.
|
|
20
|
+
*/
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { AsyncEntry } from '@napi-rs/keyring';
|
|
23
|
+
import { decryptString, encryptString } from './token-crypto.js';
|
|
24
|
+
const SERVICE_NAME = 'borg-mcp';
|
|
25
|
+
const defaultEntryFactory = (account) => new AsyncEntry(SERVICE_NAME, account);
|
|
26
|
+
/**
|
|
27
|
+
* Build the OS-keychain backend. Preserves config.ts's prior semantics:
|
|
28
|
+
* a missing entry reads as null, and delete is silent on a NoEntry error
|
|
29
|
+
* (idempotent clear) while other errors propagate (fail-loud).
|
|
30
|
+
*/
|
|
31
|
+
export function makeKeychainBackend(entryFactory = defaultEntryFactory) {
|
|
32
|
+
return {
|
|
33
|
+
name: 'keychain',
|
|
34
|
+
async get(account) {
|
|
35
|
+
return (await entryFactory(account).getPassword()) ?? null;
|
|
36
|
+
},
|
|
37
|
+
async set(account, value) {
|
|
38
|
+
await entryFactory(account).setPassword(value);
|
|
39
|
+
},
|
|
40
|
+
async delete(account) {
|
|
41
|
+
try {
|
|
42
|
+
await entryFactory(account).deletePassword();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const msg = String(err?.message ?? '');
|
|
46
|
+
if (/no entry|not found|no matching/i.test(msg))
|
|
47
|
+
return;
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the encrypted-file backend. All accounts live in one JSON object
|
|
55
|
+
* encrypted as a single AES-256-GCM envelope at `filePath`.
|
|
56
|
+
*
|
|
57
|
+
* A missing file reads as an empty map. A file that won't decrypt (wrong
|
|
58
|
+
* machine key after a hostname change, truncation, tampering) is ALSO
|
|
59
|
+
* treated as empty: the only consequence is the user re-runs `borg setup`,
|
|
60
|
+
* which is the right fail-safe for credential material — a hard crash on a
|
|
61
|
+
* corrupt dotfile would be worse UX than transparent re-auth.
|
|
62
|
+
*/
|
|
63
|
+
export function makeEncryptedFileBackend(deps) {
|
|
64
|
+
const { filePath, key, fs } = deps;
|
|
65
|
+
async function readMap() {
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = await fs.readFile(filePath);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return {}; // missing file → no stored tokens yet
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const json = decryptString(raw.trim(), key);
|
|
75
|
+
const parsed = JSON.parse(json);
|
|
76
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return {}; // undecryptable / corrupt → fail safe to re-auth
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function writeMap(map) {
|
|
83
|
+
await fs.mkdir(path.dirname(filePath), 0o700);
|
|
84
|
+
await fs.writeFile(filePath, encryptString(JSON.stringify(map), key), 0o600);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
name: 'encrypted-file',
|
|
88
|
+
async get(account) {
|
|
89
|
+
const map = await readMap();
|
|
90
|
+
return Object.prototype.hasOwnProperty.call(map, account) ? map[account] : null;
|
|
91
|
+
},
|
|
92
|
+
async set(account, value) {
|
|
93
|
+
const map = await readMap();
|
|
94
|
+
map[account] = value;
|
|
95
|
+
await writeMap(map);
|
|
96
|
+
},
|
|
97
|
+
async delete(account) {
|
|
98
|
+
const map = await readMap();
|
|
99
|
+
if (Object.prototype.hasOwnProperty.call(map, account)) {
|
|
100
|
+
delete map[account];
|
|
101
|
+
await writeMap(map);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Select the persistent backend: a forced choice (BORG_TOKEN_STORE=keychain|file)
|
|
108
|
+
* wins and skips the probe; otherwise probe the keychain and fall back to the
|
|
109
|
+
* encrypted file when it's unavailable.
|
|
110
|
+
*/
|
|
111
|
+
export async function selectTokenBackend(deps) {
|
|
112
|
+
if (deps.forced === 'keychain')
|
|
113
|
+
return deps.makeKeychain();
|
|
114
|
+
if (deps.forced === 'file')
|
|
115
|
+
return deps.makeFile();
|
|
116
|
+
return (await deps.keyringAvailable()) ? deps.makeKeychain() : deps.makeFile();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Resolve an externally-supplied id_token (no storage). BORG_TOKEN takes
|
|
120
|
+
* precedence; otherwise BORG_TOKEN_FILE is read from disk. Returns null when
|
|
121
|
+
* neither is configured. The value is trimmed (env vars and files commonly
|
|
122
|
+
* carry trailing newlines). The caller owns this token's freshness, so it
|
|
123
|
+
* bypasses the keychain AND the expiry check in config.ts.
|
|
124
|
+
*/
|
|
125
|
+
export async function readCallerManagedIdToken(deps) {
|
|
126
|
+
const inline = deps.env.BORG_TOKEN?.trim();
|
|
127
|
+
if (inline)
|
|
128
|
+
return inline;
|
|
129
|
+
const file = deps.env.BORG_TOKEN_FILE?.trim();
|
|
130
|
+
if (file) {
|
|
131
|
+
const contents = await deps.readFile(file);
|
|
132
|
+
return contents.trim();
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../src/token-store.ts"],"names":[],"mappings":"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"}
|