@agentage/cli 0.38.0 → 0.38.1
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/cli.js +5 -3
- package/dist/commands/auth/setup.d.ts +6 -14
- package/dist/commands/auth/setup.js +9 -121
- package/dist/commands/status/status.d.ts +4 -3
- package/dist/commands/status/status.js +3 -2
- package/dist/commands/update/update.d.ts +3 -1
- package/dist/commands/update/update.js +25 -22
- package/dist/lib/auth/api.d.ts +3 -2
- package/dist/lib/auth/api.js +50 -56
- package/dist/lib/auth/env-match.js +1 -0
- package/dist/lib/auth/introspect.js +10 -11
- package/dist/lib/auth/oauth.d.ts +2 -0
- package/dist/lib/auth/oauth.js +30 -28
- package/dist/lib/auth/sign-in.d.ts +20 -0
- package/dist/lib/auth/sign-in.js +95 -0
- package/dist/lib/fs/config.d.ts +1 -0
- package/dist/lib/fs/config.js +14 -21
- package/dist/lib/fs/file-lock.d.ts +7 -2
- package/dist/lib/fs/file-lock.js +64 -74
- package/dist/lib/fs/json-file.d.ts +3 -0
- package/dist/lib/fs/json-file.js +43 -0
- package/dist/lib/net/fetch.d.ts +10 -0
- package/dist/lib/net/fetch.js +13 -0
- package/dist/lib/net/http.js +51 -23
- package/dist/lib/npm/npm-cli.d.ts +17 -0
- package/dist/lib/npm/npm-cli.js +77 -0
- package/dist/lib/status/status-info.js +1 -0
- package/dist/lib/update/update-cache.d.ts +1 -0
- package/dist/lib/update/update-cache.js +7 -11
- package/dist/lib/update/update-check.js +1 -4
- package/dist/program.d.ts +18 -1
- package/dist/program.js +75 -32
- package/package.json +1 -1
- package/dist/lib/update/update-lock.d.ts +0 -2
- package/dist/lib/update/update-lock.js +0 -109
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
4
4
|
if (nodeMajor < 22) {
|
|
5
5
|
process.stderr.write(`agentage requires Node.js >= 22 (you have v${process.versions.node})\n`);
|
|
6
|
-
process.
|
|
6
|
+
process.exitCode = 1;
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
const { run } = await import('./program.js');
|
|
10
|
+
process.exitCode = await run();
|
|
7
11
|
}
|
|
8
|
-
const { run } = await import('./program.js');
|
|
9
|
-
await run();
|
|
10
12
|
export {};
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import { type Command } from 'commander';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { exchangeCode, registerClient, revokeToken } from '../../lib/auth/oauth.js';
|
|
5
|
-
export interface SetupOptions {
|
|
2
|
+
import { type SignInOptions, type SignInResult } from '../../lib/auth/sign-in.js';
|
|
3
|
+
export interface SetupOptions extends SignInOptions {
|
|
6
4
|
disconnect?: boolean;
|
|
7
|
-
reauth?: boolean;
|
|
8
|
-
browser?: boolean;
|
|
9
5
|
}
|
|
10
6
|
export interface SetupDeps {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
revoke: typeof revokeToken;
|
|
14
|
-
startServer: typeof startCallbackServer;
|
|
15
|
-
openBrowser: (url: string) => Promise<void>;
|
|
7
|
+
ensureSignedIn: (opts: SignInOptions) => Promise<SignInResult>;
|
|
8
|
+
signOut: () => Promise<void>;
|
|
16
9
|
printStatus: () => Promise<void>;
|
|
17
|
-
introspect: typeof introspectToken;
|
|
18
10
|
}
|
|
19
|
-
export declare const runSetup: (opts: SetupOptions, deps
|
|
20
|
-
export declare const registerSetup: (program: Command) => void;
|
|
11
|
+
export declare const runSetup: (opts: SetupOptions, deps: SetupDeps) => Promise<void>;
|
|
12
|
+
export declare const registerSetup: (program: Command, action: (opts: SetupOptions) => Promise<void>) => void;
|
|
@@ -1,129 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { runStatus } from '../status/status.js';
|
|
9
|
-
const defaultDeps = {
|
|
10
|
-
register: registerClient,
|
|
11
|
-
exchange: exchangeCode,
|
|
12
|
-
revoke: revokeToken,
|
|
13
|
-
startServer: startCallbackServer,
|
|
14
|
-
openBrowser: async (url) => {
|
|
15
|
-
const { default: open } = await import('open');
|
|
16
|
-
await open(url);
|
|
17
|
-
},
|
|
18
|
-
printStatus: () => runStatus({}),
|
|
19
|
-
introspect: introspectToken,
|
|
1
|
+
import {} from 'commander';
|
|
2
|
+
import {} from '../../lib/auth/sign-in.js';
|
|
3
|
+
export const runSetup = async (opts, deps) => {
|
|
4
|
+
if (opts.disconnect)
|
|
5
|
+
return deps.signOut();
|
|
6
|
+
await deps.ensureSignedIn({ reauth: opts.reauth, browser: opts.browser });
|
|
7
|
+
await deps.printStatus();
|
|
20
8
|
};
|
|
21
|
-
const
|
|
22
|
-
siteFqdn: fqdn,
|
|
23
|
-
clientId,
|
|
24
|
-
tokens: {
|
|
25
|
-
accessToken: tokens.access_token,
|
|
26
|
-
refreshToken: tokens.refresh_token,
|
|
27
|
-
expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined,
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
const disconnect = async (deps) => {
|
|
31
|
-
const auth = readAuth();
|
|
32
|
-
if (!auth) {
|
|
33
|
-
console.log('Nothing to disconnect.');
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
await deps.revoke(links(auth.siteFqdn).auth, auth.tokens.accessToken);
|
|
37
|
-
await deleteAuth();
|
|
38
|
-
console.log('Disconnected - local credentials removed.');
|
|
39
|
-
};
|
|
40
|
-
// Validate the stored session instead of trusting mere token presence. Returns true to short-circuit
|
|
41
|
-
// (valid or a transient blip - creds intact), false to proceed into a fresh sign-in (truly expired or
|
|
42
|
-
// the credential belongs to a different target than the current run).
|
|
43
|
-
const alreadySignedIn = async (auth, deps) => {
|
|
44
|
-
const mismatch = detectEnvMismatch(auth, siteFqdn());
|
|
45
|
-
if (mismatch) {
|
|
46
|
-
console.log(`Signed in to ${mismatch.credentialFqdn} (${mismatch.credentialEnv}), but this run targets ` +
|
|
47
|
-
`${mismatch.targetFqdn} (${mismatch.targetEnv}) - signing you into ${mismatch.targetEnv}.\n`);
|
|
48
|
-
return false; // proceed to a fresh sign-in against the current target
|
|
49
|
-
}
|
|
50
|
-
try {
|
|
51
|
-
await deps.introspect(auth, links(auth.siteFqdn));
|
|
52
|
-
console.log(`Already signed in. Run ${chalk.white('agentage setup --reauth')} to sign in again.\n`);
|
|
53
|
-
await deps.printStatus();
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
if (err instanceof AuthRequiredError) {
|
|
58
|
-
console.log('Session expired - signing you in again.\n');
|
|
59
|
-
return false; // fall through to the sign-in flow, no --reauth needed
|
|
60
|
-
}
|
|
61
|
-
// Transient: do not force reauth on a blip; keep credentials and show status.
|
|
62
|
-
console.log('You appear signed in (could not fully verify - temporary).\n');
|
|
63
|
-
await deps.printStatus();
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
export const runSetup = async (opts, deps = defaultDeps) => {
|
|
68
|
-
if (opts.disconnect) {
|
|
69
|
-
await disconnect(deps);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const stored = readAuth();
|
|
73
|
-
if (stored && !opts.reauth && (await alreadySignedIn(stored, deps)))
|
|
74
|
-
return;
|
|
75
|
-
const fqdn = siteFqdn();
|
|
76
|
-
const target = links(fqdn);
|
|
77
|
-
ensureConfigDir();
|
|
78
|
-
const { verifier, challenge } = pkcePair();
|
|
79
|
-
const state = randomState();
|
|
80
|
-
const server = await deps.startServer(state);
|
|
81
|
-
try {
|
|
82
|
-
const clientId = await deps.register(target.auth, server.redirectUri);
|
|
83
|
-
const url = buildAuthorizeUrl(target.auth, {
|
|
84
|
-
clientId,
|
|
85
|
-
redirectUri: server.redirectUri,
|
|
86
|
-
challenge,
|
|
87
|
-
state,
|
|
88
|
-
});
|
|
89
|
-
console.log(`\nSign in to agentage (${fqdn}):\n\n ${url}\n`);
|
|
90
|
-
if (opts.browser === false)
|
|
91
|
-
console.log('Open the URL above in a browser to continue.');
|
|
92
|
-
else
|
|
93
|
-
await deps
|
|
94
|
-
.openBrowser(url)
|
|
95
|
-
.catch(() => console.log('Could not open a browser - use the URL above.'));
|
|
96
|
-
console.log('Waiting for sign-in to complete...');
|
|
97
|
-
const code = await server.waitForCode();
|
|
98
|
-
const tokens = await deps.exchange(target.auth, {
|
|
99
|
-
clientId,
|
|
100
|
-
code,
|
|
101
|
-
redirectUri: server.redirectUri,
|
|
102
|
-
verifier,
|
|
103
|
-
});
|
|
104
|
-
await mutateAuth(() => toAuthState(fqdn, clientId, tokens));
|
|
105
|
-
console.log(chalk.green('\nSigned in.\n'));
|
|
106
|
-
await deps.printStatus();
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
server.close();
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
export const registerSetup = (program) => {
|
|
9
|
+
export const registerSetup = (program, action) => {
|
|
113
10
|
program
|
|
114
11
|
.command('setup')
|
|
115
12
|
.description('Sign in and connect this machine to your agentage account')
|
|
116
13
|
.option('--disconnect', 'sign out and remove local credentials')
|
|
117
14
|
.option('--reauth', 'force a fresh sign-in')
|
|
118
15
|
.option('--no-browser', 'print the sign-in URL instead of opening a browser')
|
|
119
|
-
.action(
|
|
120
|
-
try {
|
|
121
|
-
await runSetup(opts);
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
125
|
-
console.error(chalk.red(`Setup failed: ${message}`));
|
|
126
|
-
process.exitCode = 1;
|
|
127
|
-
}
|
|
128
|
-
});
|
|
16
|
+
.action(action);
|
|
129
17
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type Command } from 'commander';
|
|
2
2
|
import { type StatusReport } from '../../lib/status/status-info.js';
|
|
3
3
|
export declare const printStatus: (report: StatusReport) => void;
|
|
4
|
-
export
|
|
4
|
+
export interface StatusOptions {
|
|
5
5
|
json?: boolean;
|
|
6
6
|
token?: string;
|
|
7
|
-
}
|
|
8
|
-
export declare const
|
|
7
|
+
}
|
|
8
|
+
export declare const runStatus: (opts?: StatusOptions) => Promise<void>;
|
|
9
|
+
export declare const registerStatus: (program: Command, action: (opts: StatusOptions) => Promise<void>) => void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import {} from 'commander';
|
|
2
3
|
import { resolveAuth } from '../../lib/auth/credentials.js';
|
|
3
4
|
import { siteFqdn } from '../../lib/net/origins.js';
|
|
4
5
|
import { gatherStatus } from '../../lib/status/status-info.js';
|
|
@@ -54,11 +55,11 @@ export const runStatus = async (opts = {}) => {
|
|
|
54
55
|
}
|
|
55
56
|
printStatus(report);
|
|
56
57
|
};
|
|
57
|
-
export const registerStatus = (program) => {
|
|
58
|
+
export const registerStatus = (program, action) => {
|
|
58
59
|
program
|
|
59
60
|
.command('status')
|
|
60
61
|
.description('Show CLI, account, and endpoint status')
|
|
61
62
|
.option('--json', 'machine-readable output')
|
|
62
63
|
.option('--token <token>', 'authenticate with a personal access token (aga_...); or set AGENTAGE_TOKEN')
|
|
63
|
-
.action(
|
|
64
|
+
.action(action);
|
|
64
65
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Command } from 'commander';
|
|
2
|
+
import { requireNpm10, runNpm } from '../../lib/npm/npm-cli.js';
|
|
2
3
|
import { type UpdateInfo } from '../../lib/update/update-check.js';
|
|
4
|
+
export declare const installLatest: (run?: typeof runNpm, check?: typeof requireNpm10) => Promise<void>;
|
|
3
5
|
export interface UpdateDeps {
|
|
4
6
|
check: (installed: string) => Promise<UpdateInfo>;
|
|
5
7
|
install: () => Promise<void>;
|
|
@@ -11,4 +13,4 @@ export interface UpdateOptions {
|
|
|
11
13
|
check?: boolean;
|
|
12
14
|
}
|
|
13
15
|
export declare const runUpdate: (opts: UpdateOptions, deps?: UpdateDeps) => Promise<void>;
|
|
14
|
-
export declare const registerUpdate: (program: Command) => void;
|
|
16
|
+
export declare const registerUpdate: (program: Command, action: (opts: UpdateOptions) => Promise<void>) => void;
|
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from 'node:util';
|
|
1
|
+
import { join } from 'node:path';
|
|
3
2
|
import chalk from 'chalk';
|
|
3
|
+
import {} from 'commander';
|
|
4
|
+
import { getConfigDir } from '../../lib/fs/config.js';
|
|
5
|
+
import { acquireFileLock, releaseFileLock } from '../../lib/fs/file-lock.js';
|
|
6
|
+
import { requireNpm10, runNpm } from '../../lib/npm/npm-cli.js';
|
|
4
7
|
import { checkForUpdate, INSTALL_HINT } from '../../lib/update/update-check.js';
|
|
5
|
-
import { acquireUpdateLock, releaseUpdateLock } from '../../lib/update/update-lock.js';
|
|
6
8
|
import { VERSION } from '../../utils/version.js';
|
|
7
|
-
const
|
|
9
|
+
const INSTALL_TIMEOUT_MS = 5 * 60_000;
|
|
10
|
+
const NPM_TAIL_LINES = 10;
|
|
11
|
+
// An install takes minutes, so the crashed-holder ages scale up from the config-write defaults.
|
|
12
|
+
const UPDATE_LOCK = { ttlMs: 10 * 60_000, hardStaleMs: 30 * 60_000 };
|
|
13
|
+
const updateLockTarget = () => join(getConfigDir(), 'update');
|
|
14
|
+
const tail = (text) => text.trimEnd().split('\n').slice(-NPM_TAIL_LINES).join('\n');
|
|
15
|
+
export const installLatest = async (run = runNpm, check = requireNpm10) => {
|
|
16
|
+
await check(run);
|
|
17
|
+
const res = await run(['install', '-g', '@agentage/cli@latest'], {
|
|
18
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
19
|
+
});
|
|
20
|
+
if (res.code !== 0)
|
|
21
|
+
throw new Error(`npm install failed (exit ${res.code}):\n${tail(res.stderr || res.stdout)}`);
|
|
22
|
+
};
|
|
8
23
|
const defaultDeps = {
|
|
9
24
|
check: checkForUpdate,
|
|
10
|
-
install:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
acquireLock: () => acquireUpdateLock(),
|
|
14
|
-
releaseLock: releaseUpdateLock,
|
|
25
|
+
install: () => installLatest(),
|
|
26
|
+
acquireLock: () => acquireFileLock(updateLockTarget(), Date.now(), UPDATE_LOCK),
|
|
27
|
+
releaseLock: () => releaseFileLock(updateLockTarget()),
|
|
15
28
|
log: (msg) => console.log(msg),
|
|
16
29
|
};
|
|
17
30
|
const describe = (info) => {
|
|
@@ -32,9 +45,7 @@ const install = async (deps) => {
|
|
|
32
45
|
await deps.install();
|
|
33
46
|
deps.log(chalk.green('Updated. Run `agentage status` to confirm.'));
|
|
34
47
|
};
|
|
35
|
-
// --check
|
|
36
|
-
// we're behind (available/unsupported), guarded by a single-writer lock; current/unreachable just
|
|
37
|
-
// report.
|
|
48
|
+
// --check never installs; otherwise install only when behind, under a single-writer lock.
|
|
38
49
|
export const runUpdate = async (opts, deps = defaultDeps) => {
|
|
39
50
|
const info = await deps.check(VERSION);
|
|
40
51
|
deps.log(describe(info));
|
|
@@ -52,18 +63,10 @@ export const runUpdate = async (opts, deps = defaultDeps) => {
|
|
|
52
63
|
deps.releaseLock();
|
|
53
64
|
}
|
|
54
65
|
};
|
|
55
|
-
export const registerUpdate = (program) => {
|
|
66
|
+
export const registerUpdate = (program, action) => {
|
|
56
67
|
program
|
|
57
68
|
.command('update')
|
|
58
69
|
.description('Update @agentage/cli to the latest published version')
|
|
59
70
|
.option('--check', 'report whether an update is available, without installing')
|
|
60
|
-
.action(
|
|
61
|
-
try {
|
|
62
|
-
await runUpdate(opts);
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
66
|
-
process.exitCode = 1;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
71
|
+
.action(action);
|
|
69
72
|
};
|
package/dist/lib/auth/api.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type AuthState } from '../fs/config.js';
|
|
2
|
+
import { type CliRequestInit } from '../net/fetch.js';
|
|
2
3
|
import { type Links } from '../net/origins.js';
|
|
3
4
|
export { AuthRequiredError, TransientAuthError } from './auth-errors.js';
|
|
4
5
|
export declare const refreshOrThrow: (auth: AuthState, links: Links) => Promise<void>;
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
6
|
+
export declare const authedFetch: (auth: AuthState, links: Links, url: string, init?: CliRequestInit) => Promise<Response>;
|
|
7
|
+
export declare const authedJson: <T>(auth: AuthState, links: Links, url: string, init?: CliRequestInit) => Promise<T>;
|
|
7
8
|
export { introspectToken, type TokenSession } from './introspect.js';
|
package/dist/lib/auth/api.js
CHANGED
|
@@ -1,45 +1,52 @@
|
|
|
1
1
|
import { mutateAuth } from '../fs/config.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { cliFetch, isRedirect } from '../net/fetch.js';
|
|
3
|
+
import {} from '../net/origins.js';
|
|
4
4
|
import { AuthRequiredError, isTerminalRefresh, TransientAuthError } from './auth-errors.js';
|
|
5
|
+
import { refreshTokens, TokenRequestError, tokensFrom } from './oauth.js';
|
|
5
6
|
export { AuthRequiredError, TransientAuthError } from './auth-errors.js';
|
|
6
|
-
|
|
7
|
-
const cliHeaders = () => requestHeaders();
|
|
8
|
-
// Refresh the stored token in place. On success mutates auth + persists and returns. On failure it
|
|
9
|
-
// throws: AuthRequiredError when the grant is dead (401/invalid_grant/invalid_client), else
|
|
10
|
-
// TransientAuthError (429/5xx/network/timeout) - a blip that must not be read as a dead session.
|
|
11
|
-
export const refreshOrThrow = async (auth, links) => {
|
|
12
|
-
if (auth.kind === 'pat')
|
|
13
|
-
throw new AuthRequiredError('personal access token expired or revoked - mint a new one in the dashboard (Settings -> API tokens)');
|
|
14
|
-
if (!auth.tokens.refreshToken)
|
|
15
|
-
throw new AuthRequiredError('no refresh token');
|
|
16
|
-
let fresh;
|
|
7
|
+
const requestTokens = async (auth, links, rt) => {
|
|
17
8
|
try {
|
|
18
|
-
|
|
9
|
+
return tokensFrom(await refreshTokens(links.auth, auth.clientId, rt), rt);
|
|
19
10
|
}
|
|
20
11
|
catch (err) {
|
|
21
12
|
if (err instanceof TokenRequestError && isTerminalRefresh(err.status, err.oauthError))
|
|
22
13
|
throw new AuthRequiredError('session expired');
|
|
23
14
|
throw new TransientAuthError('refresh temporarily failed');
|
|
24
15
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
expiresAt: fresh.expires_in ? Date.now() + fresh.expires_in * 1000 : undefined,
|
|
29
|
-
};
|
|
30
|
-
auth.tokens = tokens; // the caller retries its request with this in-memory copy
|
|
31
|
-
// Persist under the lock, folding the new tokens onto the freshly-read state so a concurrent
|
|
32
|
-
// writer's other fields (clientId, siteFqdn) are not clobbered by a stale in-memory copy. A null
|
|
33
|
-
// re-read means a sign-out won the race: skip the write rather than resurrect it (issue #236).
|
|
16
|
+
};
|
|
17
|
+
// Never resurrect a sign-out (null) or overwrite a newer sign-in by another client.
|
|
18
|
+
const persistTokens = async (clientId, tokens) => {
|
|
34
19
|
await mutateAuth((current) => {
|
|
35
|
-
if (!current)
|
|
20
|
+
if (!current || current.clientId !== clientId)
|
|
36
21
|
return null;
|
|
37
22
|
current.tokens = tokens;
|
|
38
23
|
return current;
|
|
39
24
|
});
|
|
40
25
|
};
|
|
41
|
-
|
|
42
|
-
|
|
26
|
+
const refreshAndPersist = async (auth, links, rt) => {
|
|
27
|
+
const tokens = await requestTokens(auth, links, rt);
|
|
28
|
+
await persistTokens(auth.clientId, tokens);
|
|
29
|
+
return tokens;
|
|
30
|
+
};
|
|
31
|
+
// Keyed by refresh token: parallel 401s share one grant instead of racing its rotation.
|
|
32
|
+
const inflight = new Map();
|
|
33
|
+
const sharedRefresh = (auth, links, rt) => {
|
|
34
|
+
const pending = inflight.get(rt);
|
|
35
|
+
if (pending)
|
|
36
|
+
return pending;
|
|
37
|
+
const next = refreshAndPersist(auth, links, rt).finally(() => inflight.delete(rt));
|
|
38
|
+
inflight.set(rt, next);
|
|
39
|
+
return next;
|
|
40
|
+
};
|
|
41
|
+
// Throws AuthRequiredError for a dead grant, TransientAuthError for a blip (429/5xx/network).
|
|
42
|
+
export const refreshOrThrow = async (auth, links) => {
|
|
43
|
+
if (auth.kind === 'pat')
|
|
44
|
+
throw new AuthRequiredError('personal access token expired or revoked - mint a new one in the dashboard (Settings -> API tokens)');
|
|
45
|
+
const rt = auth.tokens.refreshToken;
|
|
46
|
+
if (!rt)
|
|
47
|
+
throw new AuthRequiredError('no refresh token');
|
|
48
|
+
auth.tokens = await sharedRefresh(auth, links, rt); // the caller retries with this copy
|
|
49
|
+
};
|
|
43
50
|
const tryRefresh = async (auth, links) => {
|
|
44
51
|
try {
|
|
45
52
|
await refreshOrThrow(auth, links);
|
|
@@ -49,41 +56,28 @@ const tryRefresh = async (auth, links) => {
|
|
|
49
56
|
return false;
|
|
50
57
|
}
|
|
51
58
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
const withBearer = (auth, init) => ({
|
|
60
|
+
...init,
|
|
61
|
+
headers: { ...init.headers, authorization: `Bearer ${auth.tokens.accessToken}` },
|
|
62
|
+
});
|
|
63
|
+
// The raw Response after at most one refresh-and-retry on 401; callers branch on the status.
|
|
64
|
+
export const authedFetch = async (auth, links, url, init = {}) => {
|
|
65
|
+
const res = await cliFetch(url, withBearer(auth, init));
|
|
66
|
+
if (res.status !== 401 || !(await tryRefresh(auth, links)))
|
|
67
|
+
return res;
|
|
68
|
+
await res.body?.cancel();
|
|
69
|
+
return cliFetch(url, withBearer(auth, init));
|
|
70
|
+
};
|
|
71
|
+
export const authedJson = async (auth, links, url, init = {}) => {
|
|
72
|
+
const method = init.method ?? 'GET';
|
|
73
|
+
const res = await authedFetch(auth, links, url, init);
|
|
62
74
|
if (res.status === 401)
|
|
63
75
|
throw new AuthRequiredError('session expired');
|
|
64
76
|
if (isRedirect(res))
|
|
65
|
-
throw new Error(
|
|
77
|
+
throw new Error(`${method} ${url} refused redirect (${res.status})`);
|
|
66
78
|
if (!res.ok)
|
|
67
|
-
throw new Error(
|
|
79
|
+
throw new Error(`${method} ${url} failed (${res.status})`);
|
|
68
80
|
return (await res.json());
|
|
69
81
|
};
|
|
70
|
-
// A bearer POST with the same refresh-once dance as authedGet. Unlike authedGet it returns the
|
|
71
|
-
// raw Response (never throws on a non-2xx): callers branch on the status codes themselves.
|
|
72
|
-
export const authedPost = async (auth, links, url, body) => {
|
|
73
|
-
const call = () => fetch(url, {
|
|
74
|
-
method: 'POST',
|
|
75
|
-
headers: {
|
|
76
|
-
authorization: `Bearer ${auth.tokens.accessToken}`,
|
|
77
|
-
'content-type': 'application/json',
|
|
78
|
-
...cliHeaders(),
|
|
79
|
-
},
|
|
80
|
-
body: JSON.stringify(body),
|
|
81
|
-
redirect: 'manual',
|
|
82
|
-
});
|
|
83
|
-
let res = await call();
|
|
84
|
-
if (res.status === 401 && (await tryRefresh(auth, links)))
|
|
85
|
-
res = await call();
|
|
86
|
-
return res;
|
|
87
|
-
};
|
|
88
82
|
// Re-export the introspection surface so existing callers keep importing from './api.js'.
|
|
89
83
|
export { introspectToken } from './introspect.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {} from '../fs/config.js';
|
|
1
2
|
import { environment, normalizeFqdn } from '../net/origins.js';
|
|
2
3
|
// The stored credential belongs to the environment it was issued for (auth.siteFqdn); the CLI's
|
|
3
4
|
// current target comes from AGENTAGE_SITE_FQDN (production default). Introspecting a dev credential
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {} from '../fs/config.js';
|
|
2
|
+
import {} from '../net/origins.js';
|
|
3
|
+
import { authedJson, refreshOrThrow } from './api.js';
|
|
2
4
|
import { AuthRequiredError, TransientAuthError } from './auth-errors.js';
|
|
3
5
|
const isPast = (iso) => {
|
|
4
6
|
if (!iso)
|
|
@@ -6,17 +8,14 @@ const isPast = (iso) => {
|
|
|
6
8
|
const t = Date.parse(iso);
|
|
7
9
|
return !Number.isNaN(t) && t <= Date.now();
|
|
8
10
|
};
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
14
|
-
// Introspection over global fetch: a network error reflects as a transient (a blip must not read as
|
|
15
|
-
// a dead session), never letting the raw error escape to gatherStatus. Only a 401 stays terminal.
|
|
11
|
+
const RETRY_BACKOFF_MS = 300;
|
|
12
|
+
// Ref'd on purpose: an unref'd backoff let Node exit 13 mid-retry once nothing else held the loop.
|
|
13
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
// Any failure but a 401 is a transient: a network blip must never read as a dead session.
|
|
16
15
|
const getSession = async (auth, links) => {
|
|
17
16
|
const url = `${links.auth}/api/auth/mcp/get-session`;
|
|
18
17
|
try {
|
|
19
|
-
return await
|
|
18
|
+
return await authedJson(auth, links, url);
|
|
20
19
|
}
|
|
21
20
|
catch (err) {
|
|
22
21
|
if (err instanceof AuthRequiredError)
|
|
@@ -75,7 +74,7 @@ const introspectOnce = async (auth, links) => {
|
|
|
75
74
|
}
|
|
76
75
|
return freshenIfStale(auth, links, body);
|
|
77
76
|
};
|
|
78
|
-
// A retry-once with a short
|
|
77
|
+
// A retry-once with a short backoff absorbs a lone transient blip; a terminal error skips it.
|
|
79
78
|
export const introspectToken = async (auth, links) => {
|
|
80
79
|
try {
|
|
81
80
|
return await introspectOnce(auth, links);
|
|
@@ -83,7 +82,7 @@ export const introspectToken = async (auth, links) => {
|
|
|
83
82
|
catch (err) {
|
|
84
83
|
if (err instanceof AuthRequiredError)
|
|
85
84
|
throw err; // terminal: no point retrying a dead session
|
|
86
|
-
await sleep(
|
|
85
|
+
await sleep(RETRY_BACKOFF_MS);
|
|
87
86
|
return introspectOnce(auth, links); // one transient-only retry; a 2nd transient propagates
|
|
88
87
|
}
|
|
89
88
|
};
|
package/dist/lib/auth/oauth.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type StoredTokens } from '../fs/config.js';
|
|
1
2
|
export interface PkcePair {
|
|
2
3
|
verifier: string;
|
|
3
4
|
challenge: string;
|
|
@@ -12,6 +13,7 @@ export interface TokenResponse {
|
|
|
12
13
|
export declare const DEFAULT_SCOPE = "memory:read memory:write offline_access";
|
|
13
14
|
export declare const pkcePair: () => PkcePair;
|
|
14
15
|
export declare const randomState: () => string;
|
|
16
|
+
export declare const tokensFrom: (res: TokenResponse, previousRefresh?: string) => StoredTokens;
|
|
15
17
|
export declare const registerClient: (authUrl: string, redirectUri: string) => Promise<string>;
|
|
16
18
|
export declare const buildAuthorizeUrl: (authUrl: string, params: {
|
|
17
19
|
clientId: string;
|
package/dist/lib/auth/oauth.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const cliHeaders = () => requestHeaders();
|
|
2
|
+
import {} from '../fs/config.js';
|
|
3
|
+
import { AUTH_TIMEOUT_MS, cliFetch } from '../net/fetch.js';
|
|
5
4
|
const REGISTER_PATH = '/api/auth/mcp/register';
|
|
6
5
|
const AUTHORIZE_PATH = '/api/auth/mcp/authorize';
|
|
7
6
|
const TOKEN_PATH = '/api/auth/mcp/token';
|
|
8
7
|
const REVOKE_PATH = '/api/auth/mcp/revoke';
|
|
8
|
+
const REVOKE_TIMEOUT_MS = 3_000;
|
|
9
9
|
export const DEFAULT_SCOPE = 'memory:read memory:write offline_access';
|
|
10
10
|
export const pkcePair = () => {
|
|
11
11
|
const verifier = randomBytes(32).toString('base64url');
|
|
@@ -13,18 +13,25 @@ export const pkcePair = () => {
|
|
|
13
13
|
return { verifier, challenge };
|
|
14
14
|
};
|
|
15
15
|
export const randomState = () => randomBytes(16).toString('base64url');
|
|
16
|
+
// A refresh that does not rotate keeps the previous refresh token.
|
|
17
|
+
export const tokensFrom = (res, previousRefresh) => ({
|
|
18
|
+
accessToken: res.access_token,
|
|
19
|
+
refreshToken: res.refresh_token ?? previousRefresh,
|
|
20
|
+
expiresAt: res.expires_in ? Date.now() + res.expires_in * 1000 : undefined,
|
|
21
|
+
});
|
|
22
|
+
const registration = (redirectUri) => JSON.stringify({
|
|
23
|
+
client_name: 'agentage CLI',
|
|
24
|
+
redirect_uris: [redirectUri],
|
|
25
|
+
token_endpoint_auth_method: 'none',
|
|
26
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
27
|
+
response_types: ['code'],
|
|
28
|
+
});
|
|
16
29
|
export const registerClient = async (authUrl, redirectUri) => {
|
|
17
|
-
const res = await
|
|
30
|
+
const res = await cliFetch(`${authUrl}${REGISTER_PATH}`, {
|
|
18
31
|
method: 'POST',
|
|
19
|
-
headers: { 'content-type': 'application/json'
|
|
20
|
-
body:
|
|
21
|
-
|
|
22
|
-
redirect_uris: [redirectUri],
|
|
23
|
-
token_endpoint_auth_method: 'none',
|
|
24
|
-
grant_types: ['authorization_code', 'refresh_token'],
|
|
25
|
-
response_types: ['code'],
|
|
26
|
-
}),
|
|
27
|
-
});
|
|
32
|
+
headers: { 'content-type': 'application/json' },
|
|
33
|
+
body: registration(redirectUri),
|
|
34
|
+
}, { timeoutMs: AUTH_TIMEOUT_MS });
|
|
28
35
|
if (!res.ok)
|
|
29
36
|
throw new Error(`client registration failed (${res.status})`);
|
|
30
37
|
const body = (await res.json());
|
|
@@ -55,12 +62,13 @@ export class TokenRequestError extends Error {
|
|
|
55
62
|
this.name = 'TokenRequestError';
|
|
56
63
|
}
|
|
57
64
|
}
|
|
65
|
+
const formInit = (form) => ({
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
68
|
+
body: new URLSearchParams(form).toString(),
|
|
69
|
+
});
|
|
58
70
|
const postForm = async (url, form) => {
|
|
59
|
-
const res = await
|
|
60
|
-
method: 'POST',
|
|
61
|
-
headers: { 'content-type': 'application/x-www-form-urlencoded', ...cliHeaders() },
|
|
62
|
-
body: new URLSearchParams(form).toString(),
|
|
63
|
-
});
|
|
71
|
+
const res = await cliFetch(url, formInit(form), { timeoutMs: AUTH_TIMEOUT_MS });
|
|
64
72
|
if (!res.ok) {
|
|
65
73
|
const body = (await res.json().catch(() => null));
|
|
66
74
|
throw new TokenRequestError(res.status, body?.error);
|
|
@@ -79,18 +87,12 @@ export const refreshTokens = (authUrl, clientId, refreshToken) => postForm(`${au
|
|
|
79
87
|
refresh_token: refreshToken,
|
|
80
88
|
client_id: clientId,
|
|
81
89
|
});
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
export const revokeToken = async (authUrl, token, timeoutMs = 3000) => {
|
|
90
|
+
// Best-effort with a short cap: local credentials are removed whatever the server says.
|
|
91
|
+
export const revokeToken = async (authUrl, token, timeoutMs = REVOKE_TIMEOUT_MS) => {
|
|
85
92
|
try {
|
|
86
|
-
await
|
|
87
|
-
method: 'POST',
|
|
88
|
-
headers: { 'content-type': 'application/x-www-form-urlencoded', ...cliHeaders() },
|
|
89
|
-
body: new URLSearchParams({ token }).toString(),
|
|
90
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
91
|
-
});
|
|
93
|
+
await cliFetch(`${authUrl}${REVOKE_PATH}`, formInit({ token }), { timeoutMs });
|
|
92
94
|
}
|
|
93
95
|
catch {
|
|
94
|
-
//
|
|
96
|
+
// a stalled or refused revoke never blocks sign-out
|
|
95
97
|
}
|
|
96
98
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { startCallbackServer } from './callback-server.js';
|
|
2
|
+
import { introspectToken } from './introspect.js';
|
|
3
|
+
import { exchangeCode, registerClient, revokeToken } from './oauth.js';
|
|
4
|
+
export interface SignInOptions {
|
|
5
|
+
reauth?: boolean;
|
|
6
|
+
browser?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export type SignInResult = 'signed-in' | 'already' | 'mismatch-resigned';
|
|
9
|
+
export interface SignInDeps {
|
|
10
|
+
register: typeof registerClient;
|
|
11
|
+
exchange: typeof exchangeCode;
|
|
12
|
+
revoke: typeof revokeToken;
|
|
13
|
+
startServer: typeof startCallbackServer;
|
|
14
|
+
openBrowser: (url: string) => Promise<void>;
|
|
15
|
+
introspect: typeof introspectToken;
|
|
16
|
+
log: (msg: string) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare const signIn: (opts: SignInOptions, deps?: SignInDeps) => Promise<void>;
|
|
19
|
+
export declare const signOut: (deps?: SignInDeps) => Promise<void>;
|
|
20
|
+
export declare const ensureSignedIn: (opts: SignInOptions, deps?: SignInDeps) => Promise<SignInResult>;
|