borgmcp 0.9.56 → 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/assimilate-cmd.d.ts +2 -5
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +12 -0
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +2 -9
- package/dist/assimilate-deps.js.map +1 -1
- 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/claude.js +28 -6
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +16 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +27 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli-platform.js +1 -1
- package/dist/cli-platform.js.map +1 -1
- package/dist/codex-remote.d.ts +60 -12
- package/dist/codex-remote.d.ts.map +1 -1
- package/dist/codex-remote.js +173 -80
- package/dist/codex-remote.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/inbox-monitor.d.ts.map +1 -1
- package/dist/inbox-monitor.js +15 -0
- package/dist/inbox-monitor.js.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/setup.js +25 -7
- package/dist/setup.js.map +1 -1
- package/dist/subscription-retry.d.ts +40 -0
- package/dist/subscription-retry.d.ts.map +1 -0
- package/dist/subscription-retry.js +23 -0
- package/dist/subscription-retry.js.map +1 -0
- package/dist/templates.d.ts +1 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +59 -11
- package/dist/templates.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,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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.58",
|
|
4
4
|
"description": "Coordinate AI coding agents in shared cubes. Works with Claude Code and Codex. Create projects, assign roles, and share a live activity log.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -40,15 +40,7 @@
|
|
|
40
40
|
],
|
|
41
41
|
"author": "Theodor Storm <theodor@byteventures.se>",
|
|
42
42
|
"license": "Apache-2.0",
|
|
43
|
-
"repository": {
|
|
44
|
-
"type": "git",
|
|
45
|
-
"url": "https://github.com/theodorstorm/borg-mcp",
|
|
46
|
-
"directory": "client"
|
|
47
|
-
},
|
|
48
43
|
"homepage": "https://borgmcp.ai",
|
|
49
|
-
"bugs": {
|
|
50
|
-
"url": "https://github.com/theodorstorm/borg-mcp/issues"
|
|
51
|
-
},
|
|
52
44
|
"dependencies": {
|
|
53
45
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
54
46
|
"@napi-rs/keyring": "^1.3.0",
|