borgmcp 0.9.57 → 0.9.59
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.map +1 -1
- package/dist/assimilate-cmd.js +23 -5
- package/dist/assimilate-cmd.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 -4
- package/dist/claude.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/codex-launch.d.ts +1 -0
- package/dist/codex-launch.d.ts.map +1 -1
- package/dist/codex-launch.js +4 -2
- package/dist/codex-launch.js.map +1 -1
- package/dist/config.d.ts +13 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +140 -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/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +3 -1
- package/dist/regen-format.js.map +1 -1
- package/dist/roster-render.d.ts +1 -0
- package/dist/roster-render.d.ts.map +1 -1
- package/dist/roster-render.js +4 -1
- package/dist/roster-render.js.map +1 -1
- 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 +126 -0
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +222 -0
- package/dist/token-store.js.map +1 -0
- package/package.json +1 -9
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AA+HA;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpF;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3E;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqBzD;AAED;;;;GAIG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAI9D;AAED;;;;GAIG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAKjD;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAGxD"}
|
package/dist/config.js
CHANGED
|
@@ -1,79 +1,152 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Secure token storage
|
|
2
|
+
* Secure token storage.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - Windows: Credential Vault (Credential Manager)
|
|
8
|
-
* - Linux: Secret Service (libsecret / gnome-keyring)
|
|
4
|
+
* The public API (storeIdToken / getIdToken / getRefreshToken / clearTokens /
|
|
5
|
+
* isAuthenticated) is unchanged; what changed in gh#557 is what sits beneath
|
|
6
|
+
* it. Three storage paths, in precedence order:
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* 1. Caller-managed (read-only): if BORG_TOKEN / BORG_TOKEN_FILE supplies an
|
|
9
|
+
* id_token, it's served verbatim — no keychain, no expiry check, no
|
|
10
|
+
* refresh_token. The caller owns the token's lifecycle (CI, containers,
|
|
11
|
+
* `borg --token-file`).
|
|
12
|
+
* 2. OS keychain (default): @napi-rs/keyring — real platform at-rest
|
|
13
|
+
* encryption (macOS Keychain / Windows Credential Vault / libsecret).
|
|
14
|
+
* 3. Encrypted file (fallback): ~/.borg/credentials, AES-256-GCM under a
|
|
15
|
+
* machine-derived key, 0600. Engages only when no keychain is available
|
|
16
|
+
* (headless Linux without Secret Service). Obfuscation-grade — see
|
|
17
|
+
* token-crypto.ts.
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* to the pre-migration shape. No call sites needed to change.
|
|
19
|
+
* The persistent backend (2 or 3) is selected once per process and memoized.
|
|
20
|
+
* BORG_TOKEN_STORE=keychain|file forces the choice and skips the probe.
|
|
18
21
|
*/
|
|
19
|
-
import
|
|
20
|
-
|
|
22
|
+
import os from 'os';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import { promises as fsp } from 'fs';
|
|
25
|
+
import { isKeyringAvailable } from './auth-env.js';
|
|
26
|
+
import { deriveMachineKey } from './token-crypto.js';
|
|
27
|
+
import { makeKeychainBackend, makeEncryptedFileBackend, selectTokenBackend, readCallerManagedIdToken, } from './token-store.js';
|
|
21
28
|
const ID_TOKEN_ACCOUNT = 'google-id-token';
|
|
22
29
|
const REFRESH_TOKEN_ACCOUNT = 'google-refresh-token';
|
|
23
30
|
const TOKEN_EXPIRY_ACCOUNT = 'token-expiry';
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* underlying Rust handle isn't held longer than necessary.
|
|
28
|
-
*/
|
|
29
|
-
function entry(account) {
|
|
30
|
-
return new AsyncEntry(SERVICE_NAME, account);
|
|
31
|
+
/** Where the encrypted-file fallback lives when no keychain is available. */
|
|
32
|
+
function credentialsPath() {
|
|
33
|
+
return path.join(os.homedir(), '.borg', 'credentials');
|
|
31
34
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
/** Production fs adapter for the encrypted-file backend. */
|
|
36
|
+
const nodeFs = {
|
|
37
|
+
readFile: (filePath) => fsp.readFile(filePath, 'utf8'),
|
|
38
|
+
writeFile: async (filePath, data, mode) => {
|
|
39
|
+
// `mode` on writeFile only applies when the file is CREATED; chmod after
|
|
40
|
+
// guarantees 0600 even when rewriting an existing credentials file.
|
|
41
|
+
await fsp.writeFile(filePath, data, { mode });
|
|
42
|
+
await fsp.chmod(filePath, mode);
|
|
43
|
+
},
|
|
44
|
+
mkdir: async (dir, mode) => {
|
|
45
|
+
await fsp.mkdir(dir, { recursive: true, mode });
|
|
46
|
+
},
|
|
47
|
+
// gh#570: atomic write (temp→rename) + O_EXCL lock primitives.
|
|
48
|
+
rename: (from, to) => fsp.rename(from, to),
|
|
49
|
+
createExclusive: async (lockPath, content) => {
|
|
50
|
+
try {
|
|
51
|
+
// 'wx' = O_CREAT | O_EXCL | O_WRONLY → fails with EEXIST if the lock
|
|
52
|
+
// already exists, giving us the atomic acquire primitive.
|
|
53
|
+
await fsp.writeFile(lockPath, content, { flag: 'wx', mode: 0o600 });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err?.code === 'EEXIST')
|
|
58
|
+
return false;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
removeFile: async (filePath) => {
|
|
63
|
+
try {
|
|
64
|
+
await fsp.unlink(filePath);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err?.code !== 'ENOENT')
|
|
68
|
+
throw err; // silent if already gone
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
fileAgeMs: async (filePath) => {
|
|
72
|
+
try {
|
|
73
|
+
const stat = await fsp.stat(filePath);
|
|
74
|
+
return Date.now() - stat.mtimeMs;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
if (err?.code === 'ENOENT')
|
|
78
|
+
return null;
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
/** Map the user-facing BORG_TOKEN_STORE value to a forced backend, if valid. */
|
|
84
|
+
function parseForcedStore(value) {
|
|
85
|
+
const v = value?.trim().toLowerCase();
|
|
86
|
+
if (v === 'keychain')
|
|
87
|
+
return 'keychain';
|
|
88
|
+
if (v === 'file' || v === 'encrypted-file')
|
|
89
|
+
return 'file';
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
// Memoized persistent-backend selection (one keychain probe per process).
|
|
93
|
+
let backendPromise = null;
|
|
94
|
+
function getBackend() {
|
|
95
|
+
if (!backendPromise) {
|
|
96
|
+
backendPromise = selectTokenBackend({
|
|
97
|
+
keyringAvailable: () => isKeyringAvailable(),
|
|
98
|
+
makeKeychain: () => makeKeychainBackend(),
|
|
99
|
+
makeFile: () => makeEncryptedFileBackend({
|
|
100
|
+
filePath: credentialsPath(),
|
|
101
|
+
key: deriveMachineKey({
|
|
102
|
+
hostname: os.hostname(),
|
|
103
|
+
username: os.userInfo().username,
|
|
104
|
+
platform: process.platform,
|
|
105
|
+
}),
|
|
106
|
+
fs: nodeFs,
|
|
107
|
+
}),
|
|
108
|
+
forced: parseForcedStore(process.env.BORG_TOKEN_STORE),
|
|
109
|
+
});
|
|
51
110
|
}
|
|
111
|
+
return backendPromise;
|
|
112
|
+
}
|
|
113
|
+
/** Caller-managed id_token (BORG_TOKEN / BORG_TOKEN_FILE), or null. */
|
|
114
|
+
function callerManagedIdToken() {
|
|
115
|
+
return readCallerManagedIdToken({
|
|
116
|
+
env: process.env,
|
|
117
|
+
readFile: (filePath) => fsp.readFile(filePath, 'utf8'),
|
|
118
|
+
});
|
|
52
119
|
}
|
|
53
120
|
/**
|
|
54
|
-
* Store Google OAuth ID token securely in the
|
|
121
|
+
* Store Google OAuth ID token securely in the selected backend.
|
|
55
122
|
*/
|
|
56
123
|
export async function storeIdToken(idToken, expiresAt) {
|
|
57
|
-
await
|
|
58
|
-
await
|
|
124
|
+
const backend = await getBackend();
|
|
125
|
+
await backend.set(ID_TOKEN_ACCOUNT, idToken);
|
|
126
|
+
await backend.set(TOKEN_EXPIRY_ACCOUNT, expiresAt.toString());
|
|
59
127
|
}
|
|
60
128
|
/**
|
|
61
|
-
* Store Google OAuth refresh token securely in the
|
|
129
|
+
* Store Google OAuth refresh token securely in the selected backend.
|
|
62
130
|
*/
|
|
63
131
|
export async function storeRefreshToken(refreshToken) {
|
|
64
|
-
await
|
|
132
|
+
const backend = await getBackend();
|
|
133
|
+
await backend.set(REFRESH_TOKEN_ACCOUNT, refreshToken);
|
|
65
134
|
}
|
|
66
135
|
/**
|
|
67
|
-
* Retrieve the Google OAuth ID token
|
|
68
|
-
* Returns null if not stored or expired (5-minute buffer).
|
|
136
|
+
* Retrieve the Google OAuth ID token.
|
|
69
137
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
138
|
+
* A caller-managed token (BORG_TOKEN / BORG_TOKEN_FILE) takes precedence and
|
|
139
|
+
* is returned verbatim — the caller owns its freshness, so the expiry buffer
|
|
140
|
+
* does not apply. Otherwise reads the persistent backend and returns null if
|
|
141
|
+
* not stored or within the 5-minute expiry buffer.
|
|
73
142
|
*/
|
|
74
143
|
export async function getIdToken() {
|
|
75
|
-
const
|
|
76
|
-
|
|
144
|
+
const callerManaged = await callerManagedIdToken();
|
|
145
|
+
if (callerManaged)
|
|
146
|
+
return callerManaged;
|
|
147
|
+
const backend = await getBackend();
|
|
148
|
+
const token = await backend.get(ID_TOKEN_ACCOUNT);
|
|
149
|
+
const expiryStr = await backend.get(TOKEN_EXPIRY_ACCOUNT);
|
|
77
150
|
if (!token || !expiryStr) {
|
|
78
151
|
return null;
|
|
79
152
|
}
|
|
@@ -86,19 +159,26 @@ export async function getIdToken() {
|
|
|
86
159
|
return token;
|
|
87
160
|
}
|
|
88
161
|
/**
|
|
89
|
-
* Retrieve the Google OAuth refresh token
|
|
162
|
+
* Retrieve the Google OAuth refresh token. There is no refresh_token in
|
|
163
|
+
* caller-managed mode (the externally-supplied id_token has no refresh
|
|
164
|
+
* counterpart), so this returns null whenever a caller-managed token is set.
|
|
90
165
|
*/
|
|
91
166
|
export async function getRefreshToken() {
|
|
92
|
-
|
|
167
|
+
if (await callerManagedIdToken())
|
|
168
|
+
return null;
|
|
169
|
+
const backend = await getBackend();
|
|
170
|
+
return backend.get(REFRESH_TOKEN_ACCOUNT);
|
|
93
171
|
}
|
|
94
172
|
/**
|
|
95
|
-
* Clear all stored tokens from the
|
|
96
|
-
*
|
|
173
|
+
* Clear all stored tokens from the selected backend. Idempotent — clearing
|
|
174
|
+
* an already-empty store is a no-op. Does not touch caller-managed env vars
|
|
175
|
+
* (those are the caller's to manage).
|
|
97
176
|
*/
|
|
98
177
|
export async function clearTokens() {
|
|
99
|
-
await
|
|
100
|
-
await
|
|
101
|
-
await
|
|
178
|
+
const backend = await getBackend();
|
|
179
|
+
await backend.delete(ID_TOKEN_ACCOUNT);
|
|
180
|
+
await backend.delete(REFRESH_TOKEN_ACCOUNT);
|
|
181
|
+
await backend.delete(TOKEN_EXPIRY_ACCOUNT);
|
|
102
182
|
}
|
|
103
183
|
/**
|
|
104
184
|
* Check if user has valid authentication.
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
/**
|
|
21
|
+
* Failure of the device-grant flow. `code` is Google's OAuth error code
|
|
22
|
+
* where one exists (`access_denied`, `expired_token`, `invalid_client`,
|
|
23
|
+
* `slow_down`, `authorization_pending`) or a synthetic code for
|
|
24
|
+
* transport/shape failures (`device_code_request_failed`,
|
|
25
|
+
* `device_token_request_failed`, `malformed_token_response`).
|
|
26
|
+
*
|
|
27
|
+
* Token material is never placed in the message — only Google's error
|
|
28
|
+
* code + description, mirroring RefreshTokenInvalidError's discipline.
|
|
29
|
+
*/
|
|
30
|
+
export declare class DeviceAuthError extends Error {
|
|
31
|
+
readonly code: string;
|
|
32
|
+
constructor(code: string, message?: string);
|
|
33
|
+
}
|
|
34
|
+
export interface DeviceAuthConfig {
|
|
35
|
+
clientId: string;
|
|
36
|
+
/** Limited-Input clients are issued a secret; included in the token poll. */
|
|
37
|
+
clientSecret?: string;
|
|
38
|
+
scopes: string[];
|
|
39
|
+
/** Overridable for tests; defaults to Google's production endpoints. */
|
|
40
|
+
deviceCodeUrl?: string;
|
|
41
|
+
tokenUrl?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface DeviceAuthDeps {
|
|
44
|
+
fetch: typeof fetch;
|
|
45
|
+
sleep: (ms: number) => Promise<void>;
|
|
46
|
+
/** Monotonic-ish clock for the local expiry deadline; defaults to Date.now. */
|
|
47
|
+
now?: () => number;
|
|
48
|
+
}
|
|
49
|
+
export interface DeviceCodeResponse {
|
|
50
|
+
device_code: string;
|
|
51
|
+
user_code: string;
|
|
52
|
+
/** Google returns `verification_url` (not the RFC's `verification_uri`). */
|
|
53
|
+
verification_url: string;
|
|
54
|
+
expires_in: number;
|
|
55
|
+
interval: number;
|
|
56
|
+
}
|
|
57
|
+
export interface DeviceTokenResult {
|
|
58
|
+
id_token: string;
|
|
59
|
+
refresh_token?: string;
|
|
60
|
+
expires_in: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Step 1 — request a device_code + user_code from Google.
|
|
64
|
+
*/
|
|
65
|
+
export declare function requestDeviceCode(config: DeviceAuthConfig, deps: DeviceAuthDeps): Promise<DeviceCodeResponse>;
|
|
66
|
+
/**
|
|
67
|
+
* Step 2 — poll Google's token endpoint until the user authorizes the
|
|
68
|
+
* device_code, honoring the RFC 8628 poll semantics.
|
|
69
|
+
*
|
|
70
|
+
* Sleeps `interval` BEFORE each poll (never hammers immediately). A local
|
|
71
|
+
* deadline derived from `expires_in` bounds the loop so a code the user
|
|
72
|
+
* abandons can't poll forever even if Google never returns expired_token.
|
|
73
|
+
*/
|
|
74
|
+
export declare function pollForDeviceToken(deviceCode: DeviceCodeResponse, config: DeviceAuthConfig, deps: DeviceAuthDeps): Promise<DeviceTokenResult>;
|
|
75
|
+
//# sourceMappingURL=device-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-auth.d.ts","sourceRoot":"","sources":["../src/device-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAQH;;;;;;;;;GASG;AACH,qBAAa,eAAgB,SAAQ,KAAK;aACZ,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;CAI3D;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAiD7B;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,kBAAkB,EAC9B,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,iBAAiB,CAAC,CA4E5B"}
|
|
@@ -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/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import { startLogStream, getStreamStatus } from './log-stream.js';
|
|
|
22
22
|
import { renderRoleList } from './list-roles-render.js';
|
|
23
23
|
import { getPackageVersion, getOnDiskVersion, handleVersionFlag } from './version.js';
|
|
24
24
|
import { renderStreamStatus, checkInboxMonitorHealthy, formatWakePathPrefix, shouldShowWakePathWarning, } from './stream-status.js';
|
|
25
|
-
import { renderRoster } from './roster-render.js';
|
|
25
|
+
import { formatRoleAgentLabel, renderRoster } from './roster-render.js';
|
|
26
26
|
import { renderSyncRolesResult } from './sync-roles-render.js';
|
|
27
27
|
import { initConsolePrefix, consolePrefix } from './console-prefix.js';
|
|
28
28
|
import { isCodexRemoteWakeEnabled } from './codex-app-wake.js';
|
|
@@ -503,7 +503,7 @@ async function main() {
|
|
|
503
503
|
},
|
|
504
504
|
{
|
|
505
505
|
name: 'borg:list-drones',
|
|
506
|
-
description: 'List every drone in a cube (owner-scoped). Returns id, label, role_id, last_seen for each — gives the Coordinator a roster they can act on with borg:reassign-drone.',
|
|
506
|
+
description: 'List every drone in a cube (owner-scoped). Returns id, label, role_id, agent_kind, last_seen for each — gives the Coordinator a roster they can act on with borg:reassign-drone.',
|
|
507
507
|
inputSchema: {
|
|
508
508
|
type: 'object',
|
|
509
509
|
properties: {
|
|
@@ -1176,7 +1176,8 @@ async function main() {
|
|
|
1176
1176
|
const rolesById = new Map(roles.map((r) => [r.id, r]));
|
|
1177
1177
|
const lines = drones.map((d) => {
|
|
1178
1178
|
const r = rolesById.get(d.role_id);
|
|
1179
|
-
|
|
1179
|
+
const roleLabel = formatRoleAgentLabel(r?.name ?? '?', d.agent_kind);
|
|
1180
|
+
return `- **${d.label}** (id: ${d.id}) — role: ${roleLabel} (${d.role_id}) — last seen ${d.last_seen}`;
|
|
1180
1181
|
});
|
|
1181
1182
|
return { content: [{ type: 'text', text: `Drones in cube ${cubeId} (${drones.length}):\n\n${lines.join('\n')}` }] };
|
|
1182
1183
|
}
|