@mnemom/mnemom 0.14.6 → 0.15.0
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/dist/commands/activity.d.ts +2 -0
- package/dist/commands/{integrity.js → activity.js} +4 -1
- package/dist/commands/agents.js +18 -1
- package/dist/commands/api-key.js +20 -4
- package/dist/commands/auth.js +39 -32
- package/dist/commands/card.js +17 -2
- package/dist/commands/org.js +15 -2
- package/dist/commands/protection.d.ts +2 -1
- package/dist/commands/protection.js +163 -3
- package/dist/commands/status.js +4 -1
- package/dist/commands/try-me.d.ts +52 -0
- package/dist/commands/try-me.js +361 -0
- package/dist/index.js +63 -3
- package/dist/lib/api.js +5 -6
- package/dist/lib/auth.d.ts +33 -10
- package/dist/lib/auth.js +71 -201
- package/dist/lib/oauth.d.ts +121 -0
- package/dist/lib/oauth.js +450 -0
- package/dist/lib/try-me.d.ts +181 -0
- package/dist/lib/try-me.js +245 -0
- package/package.json +3 -2
- package/dist/commands/integrity.d.ts +0 -1
package/dist/lib/auth.js
CHANGED
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Stores auth tokens in ~/.mnemom/auth.json (UC-9: no more config.json).
|
|
5
5
|
* License JWTs are stored alongside auth tokens.
|
|
6
|
+
*
|
|
7
|
+
* As of MNE-806, `mnemom login` authenticates against the Mnemom OAuth
|
|
8
|
+
* Authorization Server (see lib/oauth.ts) and persists SCOPED OAuth tokens
|
|
9
|
+
* (access + optional refresh, scope, token_type, and the registered client_id
|
|
10
|
+
* needed to refresh) rather than a full Supabase session. The stored shape
|
|
11
|
+
* stays backward-compatible: `email`/`userId` are optional (opaque OAuth access
|
|
12
|
+
* tokens don't carry identity) and pre-existing Supabase sessions on disk
|
|
13
|
+
* continue to resolve until they expire.
|
|
6
14
|
*/
|
|
7
15
|
import * as fs from "node:fs";
|
|
8
16
|
import * as path from "node:path";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import { exec } from "node:child_process";
|
|
12
|
-
import { getApiUrl, getWebsiteUrl, MNEMOM_DIR } from "./config.js";
|
|
17
|
+
import { MNEMOM_DIR } from "./config.js";
|
|
18
|
+
import { loginWithLoopback, loginWithDevice, refreshTokens as oauthRefreshTokens, } from "./oauth.js";
|
|
13
19
|
// ============================================================================
|
|
14
20
|
// Auth Store (persisted to ~/.mnemom/auth.json)
|
|
15
21
|
// ============================================================================
|
|
@@ -27,12 +33,22 @@ function loadAuthStore() {
|
|
|
27
33
|
}
|
|
28
34
|
function saveAuthStore(store) {
|
|
29
35
|
if (!fs.existsSync(MNEMOM_DIR)) {
|
|
30
|
-
|
|
36
|
+
// 0700: the directory holds bearer credentials — owner-only.
|
|
37
|
+
fs.mkdirSync(MNEMOM_DIR, { recursive: true, mode: 0o700 });
|
|
31
38
|
}
|
|
32
39
|
const resolvedPath = path.resolve(AUTH_FILE);
|
|
33
40
|
const sanitized = JSON.parse(JSON.stringify(store));
|
|
34
41
|
const tmpFile = `${resolvedPath}.${process.pid}.tmp`;
|
|
35
|
-
|
|
42
|
+
// 0600 from creation — the temp file already contains the secret, so the
|
|
43
|
+
// restrictive mode must be set at write time, not after rename.
|
|
44
|
+
fs.writeFileSync(tmpFile, JSON.stringify(sanitized, null, 2), { mode: 0o600 });
|
|
45
|
+
// Belt-and-suspenders: if the file pre-existed with looser perms, tighten it.
|
|
46
|
+
try {
|
|
47
|
+
fs.chmodSync(tmpFile, 0o600);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* best effort on platforms without POSIX perms */
|
|
51
|
+
}
|
|
36
52
|
fs.renameSync(tmpFile, resolvedPath);
|
|
37
53
|
}
|
|
38
54
|
// ============================================================================
|
|
@@ -71,40 +87,16 @@ export function clearLicenseJwt() {
|
|
|
71
87
|
export function getLicenseJwt() {
|
|
72
88
|
return loadAuthStore()?.licenseJwt ?? null;
|
|
73
89
|
}
|
|
74
|
-
/**
|
|
75
|
-
function
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
* "valid" token while every authenticated API call gets 401.
|
|
85
|
-
*/
|
|
86
|
-
function jwtExpSeconds(accessToken) {
|
|
87
|
-
const parts = accessToken.split(".");
|
|
88
|
-
if (parts.length !== 3)
|
|
89
|
-
return null;
|
|
90
|
-
try {
|
|
91
|
-
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
92
|
-
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
return payload.exp;
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Compute the effective expiresAt for a freshly issued access token.
|
|
103
|
-
* Prefers the JWT's own `exp` claim; falls back to `now + expires_in` if the
|
|
104
|
-
* token can't be parsed (e.g. an opaque token).
|
|
105
|
-
*/
|
|
106
|
-
export function computeExpiresAt(accessToken, expiresInSeconds) {
|
|
107
|
-
return jwtExpSeconds(accessToken) ?? Math.floor(Date.now() / 1000) + expiresInSeconds;
|
|
90
|
+
/** Convert OAuth tokens (lib/oauth.ts) into the persisted auth-store shape. */
|
|
91
|
+
function fromOAuthTokens(tokens, clientId) {
|
|
92
|
+
return {
|
|
93
|
+
accessToken: tokens.accessToken,
|
|
94
|
+
refreshToken: tokens.refreshToken,
|
|
95
|
+
expiresAt: tokens.expiresAt,
|
|
96
|
+
scope: tokens.scope,
|
|
97
|
+
tokenType: tokens.tokenType,
|
|
98
|
+
clientId,
|
|
99
|
+
};
|
|
108
100
|
}
|
|
109
101
|
/**
|
|
110
102
|
* Get a valid access token, or null if not authenticated.
|
|
@@ -126,7 +118,7 @@ export async function getAccessToken() {
|
|
|
126
118
|
return auth.accessToken;
|
|
127
119
|
}
|
|
128
120
|
// Auto-refresh
|
|
129
|
-
const refreshed = await
|
|
121
|
+
const refreshed = await refreshStoredTokens(auth);
|
|
130
122
|
if (refreshed)
|
|
131
123
|
return refreshed.accessToken;
|
|
132
124
|
return null;
|
|
@@ -147,7 +139,7 @@ export async function forceRefreshAccessToken() {
|
|
|
147
139
|
const auth = getAuthInfo();
|
|
148
140
|
if (!auth?.refreshToken)
|
|
149
141
|
return null;
|
|
150
|
-
const refreshed = await
|
|
142
|
+
const refreshed = await refreshStoredTokens(auth);
|
|
151
143
|
return refreshed?.accessToken ?? null;
|
|
152
144
|
}
|
|
153
145
|
/**
|
|
@@ -203,171 +195,49 @@ export async function isLoggedIn() {
|
|
|
203
195
|
return cred.type !== "none";
|
|
204
196
|
}
|
|
205
197
|
// ============================================================================
|
|
206
|
-
//
|
|
198
|
+
// Interactive login (OAuth 2.1 against the Mnemom Authorization Server)
|
|
207
199
|
// ============================================================================
|
|
200
|
+
/**
|
|
201
|
+
* Interactive browser login: OAuth 2.1 authorization-code + PKCE with a
|
|
202
|
+
* loopback redirect (the `wrangler login` pattern). Persists the resulting
|
|
203
|
+
* SCOPED tokens (plus the client_id needed to refresh) to ~/.mnemom/auth.json.
|
|
204
|
+
*/
|
|
208
205
|
export async function loginWithBrowser() {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
console.log("Opening browser to authenticate...");
|
|
214
|
-
console.log(`If the browser doesn't open, visit:\n ${loginUrl}\n`);
|
|
215
|
-
openBrowser(loginUrl);
|
|
216
|
-
console.log("Waiting for authentication...");
|
|
217
|
-
try {
|
|
218
|
-
const tokens = await tokenPromise;
|
|
219
|
-
saveAuthTokens(tokens);
|
|
220
|
-
return tokens;
|
|
221
|
-
}
|
|
222
|
-
finally {
|
|
223
|
-
close();
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
async function startCallbackServer(expectedState) {
|
|
227
|
-
let resolveTokens;
|
|
228
|
-
let rejectTokens;
|
|
229
|
-
const tokenPromise = new Promise((resolve, reject) => {
|
|
230
|
-
resolveTokens = resolve;
|
|
231
|
-
rejectTokens = reject;
|
|
232
|
-
});
|
|
233
|
-
const server = http.createServer((req, res) => {
|
|
234
|
-
if (req.method === "OPTIONS") {
|
|
235
|
-
res.writeHead(200, {
|
|
236
|
-
"Access-Control-Allow-Origin": "*",
|
|
237
|
-
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
238
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
239
|
-
});
|
|
240
|
-
res.end();
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (req.method !== "POST" || req.url !== "/callback") {
|
|
244
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
245
|
-
res.end("Not found");
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
let body = "";
|
|
249
|
-
req.on("data", (chunk) => {
|
|
250
|
-
body += chunk.toString();
|
|
251
|
-
if (body.length > 1_000_000) {
|
|
252
|
-
req.destroy();
|
|
253
|
-
rejectTokens(new Error("Callback body too large"));
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
req.on("end", () => {
|
|
257
|
-
try {
|
|
258
|
-
const data = JSON.parse(body);
|
|
259
|
-
if (data.state !== expectedState) {
|
|
260
|
-
res.writeHead(403, {
|
|
261
|
-
"Content-Type": "text/html",
|
|
262
|
-
"Access-Control-Allow-Origin": "*",
|
|
263
|
-
});
|
|
264
|
-
res.end("<html><body><h2>Authentication failed</h2><p>State mismatch.</p></body></html>");
|
|
265
|
-
rejectTokens(new Error("State mismatch — possible CSRF attack"));
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
const tokens = {
|
|
269
|
-
accessToken: data.access_token,
|
|
270
|
-
refreshToken: data.refresh_token,
|
|
271
|
-
expiresAt: computeExpiresAt(data.access_token, data.expires_in),
|
|
272
|
-
userId: data.user_id,
|
|
273
|
-
email: data.user_email,
|
|
274
|
-
};
|
|
275
|
-
res.writeHead(200, {
|
|
276
|
-
"Content-Type": "text/html",
|
|
277
|
-
"Access-Control-Allow-Origin": "*",
|
|
278
|
-
});
|
|
279
|
-
res.end(`<html><body style="font-family:system-ui;text-align:center;padding:60px">
|
|
280
|
-
<h2>Authenticated!</h2>
|
|
281
|
-
<p>You can close this tab and return to the terminal.</p>
|
|
282
|
-
</body></html>`);
|
|
283
|
-
resolveTokens(tokens);
|
|
284
|
-
}
|
|
285
|
-
catch {
|
|
286
|
-
res.writeHead(400, {
|
|
287
|
-
"Content-Type": "text/html",
|
|
288
|
-
"Access-Control-Allow-Origin": "*",
|
|
289
|
-
});
|
|
290
|
-
res.end("<html><body><h2>Authentication failed</h2><p>Invalid callback data.</p></body></html>");
|
|
291
|
-
rejectTokens(new Error("Invalid callback data"));
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
const port = await new Promise((resolve) => {
|
|
296
|
-
server.listen(0, "127.0.0.1", () => {
|
|
297
|
-
resolve(server.address().port);
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
const timeout = setTimeout(() => {
|
|
301
|
-
rejectTokens(new Error("Login timed out. Please try again."));
|
|
302
|
-
server.close();
|
|
303
|
-
}, 5 * 60 * 1000);
|
|
304
|
-
return {
|
|
305
|
-
port,
|
|
306
|
-
tokenPromise,
|
|
307
|
-
close: () => {
|
|
308
|
-
clearTimeout(timeout);
|
|
309
|
-
server.close();
|
|
310
|
-
},
|
|
311
|
-
};
|
|
206
|
+
const { tokens, clientId } = await loginWithLoopback();
|
|
207
|
+
const stored = fromOAuthTokens(tokens, clientId);
|
|
208
|
+
saveAuthTokens(stored);
|
|
209
|
+
return stored;
|
|
312
210
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
method: "POST",
|
|
324
|
-
headers: { "Content-Type": "application/json" },
|
|
325
|
-
body: JSON.stringify({ email, password }),
|
|
326
|
-
});
|
|
327
|
-
if (!res.ok) {
|
|
328
|
-
const body = (await res.json().catch(() => ({})));
|
|
329
|
-
throw new Error(body.error || body.message || "Login failed");
|
|
330
|
-
}
|
|
331
|
-
const data = (await res.json());
|
|
332
|
-
const tokens = {
|
|
333
|
-
accessToken: data.access_token,
|
|
334
|
-
refreshToken: data.refresh_token,
|
|
335
|
-
expiresAt: computeExpiresAt(data.access_token, data.expires_in),
|
|
336
|
-
userId: data.user.id,
|
|
337
|
-
email: data.user.email,
|
|
338
|
-
};
|
|
339
|
-
saveAuthTokens(tokens);
|
|
340
|
-
return tokens;
|
|
211
|
+
/**
|
|
212
|
+
* Headless login: RFC 8628 device authorization grant. Shows a user_code +
|
|
213
|
+
* verification_uri for the user to approve in any browser (possibly on another
|
|
214
|
+
* device), polls until approval, and persists the scoped tokens.
|
|
215
|
+
*/
|
|
216
|
+
export async function loginWithDeviceFlow() {
|
|
217
|
+
const { tokens, clientId } = await loginWithDevice();
|
|
218
|
+
const stored = fromOAuthTokens(tokens, clientId);
|
|
219
|
+
saveAuthTokens(stored);
|
|
220
|
+
return stored;
|
|
341
221
|
}
|
|
342
222
|
// ============================================================================
|
|
343
|
-
// Token refresh
|
|
223
|
+
// Token refresh (OAuth refresh_token grant)
|
|
344
224
|
// ============================================================================
|
|
345
|
-
|
|
346
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Refresh the stored tokens via the OAuth refresh_token grant. Requires both a
|
|
227
|
+
* refresh token and the client_id they were issued to; returns null (rather
|
|
228
|
+
* than throwing) when refresh isn't possible so callers degrade to re-login.
|
|
229
|
+
*/
|
|
230
|
+
async function refreshStoredTokens(auth) {
|
|
231
|
+
if (!auth.refreshToken || !auth.clientId)
|
|
347
232
|
return null;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
const res = await fetch(url, {
|
|
352
|
-
method: "POST",
|
|
353
|
-
headers: { "Content-Type": "application/json" },
|
|
354
|
-
body: sanitizeForHttp(JSON.stringify({ refresh_token: String(refreshToken) })),
|
|
355
|
-
});
|
|
356
|
-
if (!res.ok)
|
|
357
|
-
return null;
|
|
358
|
-
const data = (await res.json());
|
|
359
|
-
const existing = getAuthInfo();
|
|
360
|
-
const tokens = {
|
|
361
|
-
accessToken: data.access_token,
|
|
362
|
-
refreshToken: data.refresh_token,
|
|
363
|
-
expiresAt: computeExpiresAt(data.access_token, data.expires_in),
|
|
364
|
-
userId: existing?.userId ?? "",
|
|
365
|
-
email: existing?.email ?? "",
|
|
366
|
-
};
|
|
367
|
-
saveAuthTokens(tokens);
|
|
368
|
-
return tokens;
|
|
369
|
-
}
|
|
370
|
-
catch {
|
|
233
|
+
const refreshed = await oauthRefreshTokens(auth.refreshToken, auth.clientId);
|
|
234
|
+
if (!refreshed)
|
|
371
235
|
return null;
|
|
372
|
-
|
|
236
|
+
const stored = fromOAuthTokens(refreshed, auth.clientId);
|
|
237
|
+
// Preserve any legacy identity fields so whoami/status keep working across a
|
|
238
|
+
// refresh of a token set that still carries them.
|
|
239
|
+
stored.userId = auth.userId;
|
|
240
|
+
stored.email = auth.email;
|
|
241
|
+
saveAuthTokens(stored);
|
|
242
|
+
return stored;
|
|
373
243
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 client for the Mnemom Authorization Server.
|
|
3
|
+
*
|
|
4
|
+
* `mnemom login` authenticates against the Mnemom OAuth AS (the same AS that
|
|
5
|
+
* fronts the MCP control plane) instead of doing a raw Supabase session login.
|
|
6
|
+
* This yields SCOPED access tokens (`mcp:read` / `mcp:write`) — not the full
|
|
7
|
+
* Supabase god-token — so a leaked CLI credential can only do what the CLI was
|
|
8
|
+
* granted, and grants are independently revocable server-side (MNE-805/806).
|
|
9
|
+
*
|
|
10
|
+
* Two interactive flows, both standards-based:
|
|
11
|
+
* - Authorization Code + PKCE with a loopback redirect (the `wrangler login`
|
|
12
|
+
* pattern): open the browser to /authorize, catch the redirect on a
|
|
13
|
+
* localhost listener, exchange code+verifier at /token.
|
|
14
|
+
* - RFC 8628 Device Authorization Grant (`--no-browser` / headless): POST
|
|
15
|
+
* /device_authorization, print the user_code + verification_uri, poll
|
|
16
|
+
* /token honoring `authorization_pending` and `slow_down`.
|
|
17
|
+
*
|
|
18
|
+
* Everything is driven off RFC 8414 discovery
|
|
19
|
+
* (/.well-known/oauth-authorization-server) — endpoint paths are NOT hardcoded.
|
|
20
|
+
* The public client_id is obtained via RFC 7591 Dynamic Client Registration and
|
|
21
|
+
* cached, so there is no client secret to embed (the AS advertises
|
|
22
|
+
* token_endpoint_auth_method "none" — public clients only).
|
|
23
|
+
*/
|
|
24
|
+
export interface AuthServerMetadata {
|
|
25
|
+
issuer: string;
|
|
26
|
+
authorization_endpoint: string;
|
|
27
|
+
token_endpoint: string;
|
|
28
|
+
device_authorization_endpoint?: string;
|
|
29
|
+
registration_endpoint?: string;
|
|
30
|
+
revocation_endpoint?: string;
|
|
31
|
+
scopes_supported?: string[];
|
|
32
|
+
grant_types_supported?: string[];
|
|
33
|
+
code_challenge_methods_supported?: string[];
|
|
34
|
+
token_endpoint_auth_methods_supported?: string[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Fetch (and process-cache) the AS metadata document. We resolve it relative to
|
|
38
|
+
* the active API base URL so staging/local point at their own AS.
|
|
39
|
+
*/
|
|
40
|
+
export declare function discover(): Promise<AuthServerMetadata>;
|
|
41
|
+
/** Reset the process-level discovery cache (used by tests). */
|
|
42
|
+
export declare function _resetDiscoveryCache(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Register a public client for the CLI and return its client_id. The AS issues
|
|
45
|
+
* public clients (token_endpoint_auth_method "none"), so there is no secret to
|
|
46
|
+
* persist. The caller is responsible for caching the returned id.
|
|
47
|
+
*/
|
|
48
|
+
export declare function registerClient(redirectUris: string[]): Promise<string>;
|
|
49
|
+
export interface Pkce {
|
|
50
|
+
verifier: string;
|
|
51
|
+
challenge: string;
|
|
52
|
+
method: "S256";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generate a PKCE verifier/challenge pair. The verifier is a high-entropy
|
|
56
|
+
* URL-safe string (RFC 7636 §4.1: 43–128 chars from the unreserved set); the
|
|
57
|
+
* challenge is BASE64URL(SHA256(verifier)) for the S256 method (the only method
|
|
58
|
+
* the AS advertises, and the only one allowed under OAuth 2.1).
|
|
59
|
+
*/
|
|
60
|
+
export declare function generatePkce(): Pkce;
|
|
61
|
+
export interface OAuthTokenResponse {
|
|
62
|
+
access_token: string;
|
|
63
|
+
token_type?: string;
|
|
64
|
+
expires_in?: number;
|
|
65
|
+
refresh_token?: string;
|
|
66
|
+
scope?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface OAuthTokens {
|
|
69
|
+
accessToken: string;
|
|
70
|
+
tokenType: string;
|
|
71
|
+
refreshToken?: string;
|
|
72
|
+
scope?: string;
|
|
73
|
+
/** Unix seconds. */
|
|
74
|
+
expiresAt: number;
|
|
75
|
+
}
|
|
76
|
+
export interface LoopbackResult {
|
|
77
|
+
tokens: OAuthTokens;
|
|
78
|
+
clientId: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Run the interactive authorization-code + PKCE login. Returns the issued
|
|
82
|
+
* scoped tokens plus the client_id that was used (so the caller can persist it
|
|
83
|
+
* for refresh). `openUrl` is injectable so tests can drive the flow without
|
|
84
|
+
* spawning a real browser.
|
|
85
|
+
*/
|
|
86
|
+
export declare function loginWithLoopback(openUrl?: (url: string) => void): Promise<LoopbackResult>;
|
|
87
|
+
export interface DeviceResult {
|
|
88
|
+
tokens: OAuthTokens;
|
|
89
|
+
clientId: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Run the RFC 8628 device authorization grant. Registers a client (the device
|
|
93
|
+
* flow needs no redirect, but the AS's DCR validation requires a valid
|
|
94
|
+
* redirect_uri — HTTPS or a loopback — so we register a loopback placeholder it
|
|
95
|
+
* will never redirect to), requests a device code, prints the user_code +
|
|
96
|
+
* verification_uri for the user to approve in any browser, then polls the token
|
|
97
|
+
* endpoint until approval — honoring `authorization_pending` and `slow_down`
|
|
98
|
+
* per the spec.
|
|
99
|
+
*
|
|
100
|
+
* `display` and `sleep` are injectable so tests can drive the poll loop
|
|
101
|
+
* deterministically without real timers or stdout.
|
|
102
|
+
*/
|
|
103
|
+
export declare function loginWithDevice(opts?: {
|
|
104
|
+
display?: (line: string) => void;
|
|
105
|
+
sleep?: (ms: number) => Promise<void>;
|
|
106
|
+
}): Promise<DeviceResult>;
|
|
107
|
+
/**
|
|
108
|
+
* Exchange a refresh token for a fresh access token. Returns null if refresh is
|
|
109
|
+
* not possible (no refresh token, or the AS rejects it — e.g. revoked/expired),
|
|
110
|
+
* so callers can fall back to prompting for re-login rather than crashing.
|
|
111
|
+
*/
|
|
112
|
+
export declare function refreshTokens(refreshToken: string, clientId: string): Promise<OAuthTokens | null>;
|
|
113
|
+
/**
|
|
114
|
+
* Open `url` in the user's default browser WITHOUT a shell. The URL is always
|
|
115
|
+
* passed as a separate argv element (never concatenated into a command string),
|
|
116
|
+
* so shell metacharacters — `$(...)`, backticks, `\` — in the URL can never be
|
|
117
|
+
* interpreted as a command. Fire-and-forget: any failure (no opener installed,
|
|
118
|
+
* headless host) is swallowed because callers already print a manual-URL
|
|
119
|
+
* fallback, so a missing browser must not crash login.
|
|
120
|
+
*/
|
|
121
|
+
export declare function openBrowser(url: string): void;
|