axusage 2.1.0 → 3.0.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/README.md +61 -15
- package/dist/adapters/chatgpt.js +2 -2
- package/dist/adapters/claude.js +2 -2
- package/dist/adapters/gemini.js +2 -2
- package/dist/adapters/github-copilot.js +2 -1
- package/dist/cli.js +57 -41
- package/dist/commands/auth-clear-command.d.ts +2 -0
- package/dist/commands/auth-clear-command.js +62 -3
- package/dist/commands/auth-setup-command.d.ts +1 -0
- package/dist/commands/auth-setup-command.js +36 -5
- package/dist/commands/auth-status-command.js +36 -4
- package/dist/commands/fetch-service-usage-with-reauth.js +2 -2
- package/dist/commands/fetch-service-usage.js +4 -2
- package/dist/commands/run-auth-setup.js +26 -5
- package/dist/commands/usage-command.js +5 -3
- package/dist/config/credential-sources.d.ts +38 -0
- package/dist/config/credential-sources.js +117 -0
- package/dist/services/create-auth-context.js +2 -1
- package/dist/services/do-setup-auth.js +2 -8
- package/dist/services/get-service-access-token.d.ts +28 -0
- package/dist/services/get-service-access-token.js +146 -0
- package/dist/services/persist-storage-state.d.ts +1 -1
- package/dist/services/persist-storage-state.js +8 -8
- package/dist/services/setup-auth-flow.js +38 -11
- package/dist/services/supported-service.js +4 -2
- package/dist/services/wait-for-login.d.ts +3 -2
- package/dist/services/wait-for-login.js +89 -18
- package/dist/utils/check-cli-dependency.d.ts +25 -0
- package/dist/utils/check-cli-dependency.js +81 -0
- package/dist/utils/color.d.ts +5 -0
- package/dist/utils/color.js +27 -0
- package/dist/utils/format-service-usage.js +1 -1
- package/dist/utils/resolve-prompt-capability.d.ts +1 -0
- package/dist/utils/resolve-prompt-capability.js +3 -0
- package/dist/utils/validate-root-options.d.ts +11 -0
- package/dist/utils/validate-root-options.js +18 -0
- package/dist/utils/write-atomic-json.d.ts +1 -0
- package/dist/utils/write-atomic-json.js +56 -0
- package/package.json +11 -15
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
1
|
import { BrowserAuthManager } from "../services/browser-auth-manager.js";
|
|
2
|
+
import { resolveAuthCliDependencyOrReport } from "../utils/check-cli-dependency.js";
|
|
3
|
+
import { chalk } from "../utils/color.js";
|
|
4
|
+
import { resolvePromptCapability } from "../utils/resolve-prompt-capability.js";
|
|
3
5
|
/** Timeout for authentication setup (5 minutes) */
|
|
4
6
|
const AUTH_SETUP_TIMEOUT_MS = 300_000;
|
|
5
7
|
/**
|
|
@@ -46,31 +48,46 @@ export function isAuthFailure(result) {
|
|
|
46
48
|
export async function runAuthSetup(service) {
|
|
47
49
|
// CLI-based auth cannot use browser auth flow
|
|
48
50
|
if (service === "gemini") {
|
|
51
|
+
const cliPath = resolveAuthCliDependencyOrReport("gemini");
|
|
52
|
+
if (!cliPath)
|
|
53
|
+
return false;
|
|
49
54
|
console.error(chalk.yellow("\nGemini uses CLI-based authentication managed by the Gemini CLI."));
|
|
50
55
|
console.error(chalk.gray("\nTo re-authenticate, run:"));
|
|
51
|
-
console.error(chalk.cyan(
|
|
56
|
+
console.error(chalk.cyan(` ${cliPath}`));
|
|
52
57
|
console.error(chalk.gray("\nThe Gemini CLI will guide you through the OAuth login process.\n"));
|
|
53
58
|
return false;
|
|
54
59
|
}
|
|
55
60
|
if (service === "claude") {
|
|
61
|
+
const cliPath = resolveAuthCliDependencyOrReport("claude");
|
|
62
|
+
if (!cliPath)
|
|
63
|
+
return false;
|
|
56
64
|
console.error(chalk.yellow("\nClaude uses CLI-based authentication managed by Claude Code."));
|
|
57
65
|
console.error(chalk.gray("\nTo re-authenticate, run:"));
|
|
58
|
-
console.error(chalk.cyan(
|
|
66
|
+
console.error(chalk.cyan(` ${cliPath}`));
|
|
59
67
|
console.error(chalk.gray("\nClaude Code will guide you through authentication.\n"));
|
|
60
68
|
return false;
|
|
61
69
|
}
|
|
62
70
|
if (service === "chatgpt") {
|
|
71
|
+
const cliPath = resolveAuthCliDependencyOrReport("chatgpt");
|
|
72
|
+
if (!cliPath)
|
|
73
|
+
return false;
|
|
63
74
|
console.error(chalk.yellow("\nChatGPT uses CLI-based authentication managed by Codex."));
|
|
64
75
|
console.error(chalk.gray("\nTo re-authenticate, run:"));
|
|
65
|
-
console.error(chalk.cyan(
|
|
76
|
+
console.error(chalk.cyan(` ${cliPath}`));
|
|
66
77
|
console.error(chalk.gray("\nCodex will guide you through authentication.\n"));
|
|
67
78
|
return false;
|
|
68
79
|
}
|
|
80
|
+
if (!resolvePromptCapability()) {
|
|
81
|
+
console.error(chalk.red("Error: Interactive authentication requires a TTY terminal."));
|
|
82
|
+
console.error(chalk.gray("Re-run in a TTY terminal (avoid piping stdin/stdout) with --interactive to complete authentication."));
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
69
85
|
const manager = new BrowserAuthManager({ headless: false });
|
|
86
|
+
let setupPromise;
|
|
70
87
|
let timeoutId;
|
|
71
88
|
try {
|
|
72
89
|
console.error(chalk.blue(`\nOpening browser for ${service} authentication...\n`));
|
|
73
|
-
|
|
90
|
+
setupPromise = manager.setupAuth(service);
|
|
74
91
|
const timeoutPromise = new Promise((_, reject) => {
|
|
75
92
|
timeoutId = setTimeout(() => {
|
|
76
93
|
reject(new Error("Authentication setup timed out after 5 minutes"));
|
|
@@ -86,6 +103,10 @@ export async function runAuthSetup(service) {
|
|
|
86
103
|
}
|
|
87
104
|
finally {
|
|
88
105
|
clearTimeout(timeoutId);
|
|
106
|
+
if (setupPromise) {
|
|
107
|
+
// Avoid unhandled rejections if the timeout wins the race.
|
|
108
|
+
void setupPromise.catch(() => { });
|
|
109
|
+
}
|
|
89
110
|
await manager.close();
|
|
90
111
|
}
|
|
91
112
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
1
|
import { formatServiceUsageData, formatServiceUsageDataAsJson, formatServiceUsageAsTsv, toJsonObject, } from "../utils/format-service-usage.js";
|
|
3
2
|
import { formatPrometheusMetrics } from "../utils/format-prometheus-metrics.js";
|
|
4
3
|
import { fetchServiceUsage, selectServicesToQuery, } from "./fetch-service-usage.js";
|
|
5
4
|
import { fetchServiceUsageWithAutoReauth } from "./fetch-service-usage-with-reauth.js";
|
|
6
5
|
import { isAuthFailure } from "./run-auth-setup.js";
|
|
6
|
+
import { chalk } from "../utils/color.js";
|
|
7
7
|
/**
|
|
8
8
|
* Fetches usage for services using hybrid strategy:
|
|
9
9
|
* 1. Try all services in parallel first (fast path for valid credentials)
|
|
@@ -79,7 +79,9 @@ export async function usageCommand(options) {
|
|
|
79
79
|
}
|
|
80
80
|
if (!interactive && authFailureServices.size > 0) {
|
|
81
81
|
const list = [...authFailureServices].join(", ");
|
|
82
|
-
console.error(chalk.gray(`Authentication required for: ${list}.
|
|
82
|
+
console.error(chalk.gray(`Authentication required for: ${list}. ` +
|
|
83
|
+
"For GitHub Copilot, run 'axusage --auth-setup github-copilot --interactive'. " +
|
|
84
|
+
"For CLI-auth services, run the provider CLI (claude/codex/gemini), or re-run with '--interactive' to re-authenticate during fetch."));
|
|
83
85
|
if (successes.length > 0) {
|
|
84
86
|
console.error();
|
|
85
87
|
}
|
|
@@ -141,6 +143,6 @@ export async function usageCommand(options) {
|
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
145
|
if (hasPartialFailures) {
|
|
144
|
-
process.exitCode =
|
|
146
|
+
process.exitCode = 1;
|
|
145
147
|
}
|
|
146
148
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for credential sources per service.
|
|
3
|
+
*
|
|
4
|
+
* Supports three modes:
|
|
5
|
+
* - "local": Use local credentials from axauth (default behavior)
|
|
6
|
+
* - "vault": Fetch credentials from axvault server
|
|
7
|
+
* - "auto": Try vault first if configured and credential name provided, fallback to local
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
/** Credential source type */
|
|
11
|
+
declare const CredentialSourceType: z.ZodEnum<{
|
|
12
|
+
auto: "auto";
|
|
13
|
+
local: "local";
|
|
14
|
+
vault: "vault";
|
|
15
|
+
}>;
|
|
16
|
+
type CredentialSourceType = z.infer<typeof CredentialSourceType>;
|
|
17
|
+
/** Resolved source config with normalized fields */
|
|
18
|
+
interface ResolvedSourceConfig {
|
|
19
|
+
source: CredentialSourceType;
|
|
20
|
+
name: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Service IDs that support vault credentials (API-based services) */
|
|
23
|
+
type VaultSupportedServiceId = "claude" | "chatgpt" | "gemini";
|
|
24
|
+
/**
|
|
25
|
+
* All service IDs.
|
|
26
|
+
* Note: github-copilot uses GitHub token auth, not vault credentials,
|
|
27
|
+
* so it's excluded from VaultSupportedServiceId.
|
|
28
|
+
*/
|
|
29
|
+
type ServiceId = VaultSupportedServiceId | "github-copilot";
|
|
30
|
+
/**
|
|
31
|
+
* Get the resolved source config for a specific service.
|
|
32
|
+
*
|
|
33
|
+
* @param service - Service ID (e.g., "claude", "chatgpt", "gemini")
|
|
34
|
+
* @returns Resolved config with source type and optional credential name
|
|
35
|
+
*/
|
|
36
|
+
declare function getServiceSourceConfig(service: ServiceId): ResolvedSourceConfig;
|
|
37
|
+
export type { ServiceId, VaultSupportedServiceId };
|
|
38
|
+
export { getServiceSourceConfig };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for credential sources per service.
|
|
3
|
+
*
|
|
4
|
+
* Supports three modes:
|
|
5
|
+
* - "local": Use local credentials from axauth (default behavior)
|
|
6
|
+
* - "vault": Fetch credentials from axvault server
|
|
7
|
+
* - "auto": Try vault first if configured and credential name provided, fallback to local
|
|
8
|
+
*/
|
|
9
|
+
import Conf from "conf";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
/** Credential source type */
|
|
12
|
+
const CredentialSourceType = z.enum(["auto", "local", "vault"]);
|
|
13
|
+
/** Service source config - either a string shorthand or object with name */
|
|
14
|
+
const ServiceSourceConfig = z.union([
|
|
15
|
+
CredentialSourceType,
|
|
16
|
+
z.object({
|
|
17
|
+
source: CredentialSourceType,
|
|
18
|
+
name: z.string().optional(),
|
|
19
|
+
}),
|
|
20
|
+
]);
|
|
21
|
+
/** Full sources config - map of service ID to source config */
|
|
22
|
+
const SourcesConfig = z.record(z.string(), ServiceSourceConfig);
|
|
23
|
+
// Lazy-initialized config instance
|
|
24
|
+
let configInstance;
|
|
25
|
+
function getConfig() {
|
|
26
|
+
if (!configInstance) {
|
|
27
|
+
configInstance = new Conf({
|
|
28
|
+
projectName: "axusage",
|
|
29
|
+
projectSuffix: "",
|
|
30
|
+
schema: {
|
|
31
|
+
sources: {
|
|
32
|
+
type: "object",
|
|
33
|
+
additionalProperties: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
// Migration runs once per process when the config is first initialized.
|
|
38
|
+
migrateLegacySources(configInstance);
|
|
39
|
+
}
|
|
40
|
+
return configInstance;
|
|
41
|
+
}
|
|
42
|
+
function migrateLegacySources(config) {
|
|
43
|
+
// Respect explicit new config values; never overwrite them with legacy data.
|
|
44
|
+
if (config.get("sources") !== undefined)
|
|
45
|
+
return;
|
|
46
|
+
// Conf defaults to the legacy "-nodejs" suffix, which matches older configs.
|
|
47
|
+
const legacyConfig = new Conf({
|
|
48
|
+
projectName: "axusage",
|
|
49
|
+
});
|
|
50
|
+
const legacySources = legacyConfig.get("sources");
|
|
51
|
+
if (!legacySources)
|
|
52
|
+
return;
|
|
53
|
+
const parsed = SourcesConfig.safeParse(legacySources);
|
|
54
|
+
if (!parsed.success) {
|
|
55
|
+
console.error("Warning: Legacy axusage config contains invalid sources; skipping migration. Check your legacy config and migrate manually if needed.");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
config.set("sources", parsed.data);
|
|
59
|
+
console.error("Migrated credential source configuration from legacy axusage-nodejs config path.");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get the full credential source configuration.
|
|
63
|
+
*
|
|
64
|
+
* Priority:
|
|
65
|
+
* 1. AXUSAGE_SOURCES environment variable (flat JSON)
|
|
66
|
+
* 2. Config file sources key
|
|
67
|
+
* 3. Empty object (defaults apply per-service)
|
|
68
|
+
*/
|
|
69
|
+
function getCredentialSourceConfig() {
|
|
70
|
+
// Priority 1: Environment variable
|
|
71
|
+
const environmentVariable = process.env.AXUSAGE_SOURCES;
|
|
72
|
+
if (environmentVariable) {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = SourcesConfig.parse(JSON.parse(environmentVariable));
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const reason = error instanceof SyntaxError
|
|
79
|
+
? "invalid JSON syntax"
|
|
80
|
+
: "schema validation failed";
|
|
81
|
+
console.error(`Warning: AXUSAGE_SOURCES ${reason}, falling back to config file`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Priority 2: Config file
|
|
85
|
+
const config = getConfig();
|
|
86
|
+
const fileConfig = config.get("sources");
|
|
87
|
+
if (fileConfig) {
|
|
88
|
+
const parsed = SourcesConfig.safeParse(fileConfig);
|
|
89
|
+
if (parsed.success) {
|
|
90
|
+
return parsed.data;
|
|
91
|
+
}
|
|
92
|
+
console.error("Warning: Config file contains invalid sources, using defaults");
|
|
93
|
+
}
|
|
94
|
+
// Priority 3: Empty (defaults apply)
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get the resolved source config for a specific service.
|
|
99
|
+
*
|
|
100
|
+
* @param service - Service ID (e.g., "claude", "chatgpt", "gemini")
|
|
101
|
+
* @returns Resolved config with source type and optional credential name
|
|
102
|
+
*/
|
|
103
|
+
function getServiceSourceConfig(service) {
|
|
104
|
+
const config = getCredentialSourceConfig();
|
|
105
|
+
const serviceConfig = config[service];
|
|
106
|
+
// Default: auto mode with no credential name
|
|
107
|
+
if (serviceConfig === undefined) {
|
|
108
|
+
return { source: "auto", name: undefined };
|
|
109
|
+
}
|
|
110
|
+
// String shorthand: just the source type
|
|
111
|
+
if (typeof serviceConfig === "string") {
|
|
112
|
+
return { source: serviceConfig, name: undefined };
|
|
113
|
+
}
|
|
114
|
+
// Object: source and name
|
|
115
|
+
return { source: serviceConfig.source, name: serviceConfig.name };
|
|
116
|
+
}
|
|
117
|
+
export { getServiceSourceConfig };
|
|
@@ -27,7 +27,8 @@ export async function loadStoredUserAgent(dataDirectory, service) {
|
|
|
27
27
|
export async function createAuthContext(browser, dataDirectory, service) {
|
|
28
28
|
const storageStatePath = getStorageStatePathFor(dataDirectory, service);
|
|
29
29
|
if (!existsSync(storageStatePath)) {
|
|
30
|
-
throw new Error(`No saved authentication for ${service}.
|
|
30
|
+
throw new Error(`No saved authentication for ${service}. ` +
|
|
31
|
+
`Run 'axusage --auth-setup ${service} --interactive' first.`);
|
|
31
32
|
}
|
|
32
33
|
const userAgent = await loadStoredUserAgent(dataDirectory, service);
|
|
33
34
|
return browser.newContext({ storageState: storageStatePath, userAgent });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setupAuthInContext } from "./setup-auth-flow.js";
|
|
2
|
-
import { writeFile, chmod } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { getAuthMetaPathFor } from "./auth-storage-path.js";
|
|
4
|
+
import { writeAtomicJson } from "../utils/write-atomic-json.js";
|
|
5
5
|
export async function doSetupAuth(service, context, storagePath, instructions) {
|
|
6
6
|
console.error(`\n${instructions}`);
|
|
7
7
|
console.error("Waiting for login to complete (or press Enter to continue)\n");
|
|
@@ -9,13 +9,7 @@ export async function doSetupAuth(service, context, storagePath, instructions) {
|
|
|
9
9
|
try {
|
|
10
10
|
if (userAgent) {
|
|
11
11
|
const metaPath = getAuthMetaPathFor(path.dirname(storagePath), service);
|
|
12
|
-
await
|
|
13
|
-
try {
|
|
14
|
-
await chmod(metaPath, 0o600);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
// best effort
|
|
18
|
-
}
|
|
12
|
+
await writeAtomicJson(metaPath, { userAgent }, 0o600);
|
|
19
13
|
}
|
|
20
14
|
}
|
|
21
15
|
catch {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified credential fetcher for services.
|
|
3
|
+
*
|
|
4
|
+
* Fetches access tokens based on per-service configuration:
|
|
5
|
+
* - "local": From local axauth credential store
|
|
6
|
+
* - "vault": From axvault server
|
|
7
|
+
* - "auto": Try vault first if configured, fallback to local
|
|
8
|
+
*/
|
|
9
|
+
import { type VaultSupportedServiceId } from "../config/credential-sources.js";
|
|
10
|
+
/**
|
|
11
|
+
* Get access token for a service.
|
|
12
|
+
*
|
|
13
|
+
* Uses the configured credential source for the service:
|
|
14
|
+
* - "local": Fetch from local axauth credential store
|
|
15
|
+
* - "vault": Fetch from axvault server (requires credential name)
|
|
16
|
+
* - "auto": Try vault if configured and name provided, fallback to local
|
|
17
|
+
*
|
|
18
|
+
* @param service - Service ID (e.g., "claude", "chatgpt", "gemini")
|
|
19
|
+
* @returns Access token string or undefined if not available
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const token = await getServiceAccessToken("claude");
|
|
23
|
+
* if (!token) {
|
|
24
|
+
* console.error("No credentials found for Claude");
|
|
25
|
+
* }
|
|
26
|
+
*/
|
|
27
|
+
declare function getServiceAccessToken(service: VaultSupportedServiceId): Promise<string | undefined>;
|
|
28
|
+
export { getServiceAccessToken };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified credential fetcher for services.
|
|
3
|
+
*
|
|
4
|
+
* Fetches access tokens based on per-service configuration:
|
|
5
|
+
* - "local": From local axauth credential store
|
|
6
|
+
* - "vault": From axvault server
|
|
7
|
+
* - "auto": Try vault first if configured, fallback to local
|
|
8
|
+
*/
|
|
9
|
+
import { fetchVaultCredentials, getAgentAccessToken, isVaultConfigured, } from "axauth";
|
|
10
|
+
import { getServiceSourceConfig, } from "../config/credential-sources.js";
|
|
11
|
+
/** Map service IDs to agent IDs for axauth/vault */
|
|
12
|
+
const SERVICE_TO_AGENT = {
|
|
13
|
+
claude: "claude",
|
|
14
|
+
chatgpt: "codex", // ChatGPT and Codex both use OpenAI API credentials
|
|
15
|
+
gemini: "gemini",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Extract access token from vault credentials.
|
|
19
|
+
*
|
|
20
|
+
* Different credential types store tokens differently:
|
|
21
|
+
* - oauth-credentials: access_token (Gemini style) or tokens.access_token (Codex/OpenAI style)
|
|
22
|
+
* - oauth-token: accessToken field (Claude style)
|
|
23
|
+
* - api-key: apiKey field
|
|
24
|
+
*/
|
|
25
|
+
function extractAccessToken(credentials) {
|
|
26
|
+
if (!credentials)
|
|
27
|
+
return undefined;
|
|
28
|
+
const { data } = credentials;
|
|
29
|
+
// Try accessToken first (Claude oauth-token style, camelCase)
|
|
30
|
+
if (typeof data.accessToken === "string") {
|
|
31
|
+
return data.accessToken;
|
|
32
|
+
}
|
|
33
|
+
// Try access_token at top level (Gemini oauth-credentials style, snake_case)
|
|
34
|
+
if (typeof data.access_token === "string") {
|
|
35
|
+
return data.access_token;
|
|
36
|
+
}
|
|
37
|
+
// Try tokens.access_token (Codex/OpenAI oauth-credentials style)
|
|
38
|
+
if (data.tokens &&
|
|
39
|
+
typeof data.tokens === "object" &&
|
|
40
|
+
"access_token" in data.tokens &&
|
|
41
|
+
typeof data.tokens.access_token === "string") {
|
|
42
|
+
return data.tokens.access_token;
|
|
43
|
+
}
|
|
44
|
+
// Try apiKey as fallback (api-key type)
|
|
45
|
+
if (typeof data.apiKey === "string") {
|
|
46
|
+
return data.apiKey;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Fetch access token from vault.
|
|
52
|
+
*
|
|
53
|
+
* @returns Access token string or undefined if not available
|
|
54
|
+
*/
|
|
55
|
+
async function fetchFromVault(agentId, credentialName) {
|
|
56
|
+
try {
|
|
57
|
+
const result = await fetchVaultCredentials({
|
|
58
|
+
agentId,
|
|
59
|
+
name: credentialName,
|
|
60
|
+
});
|
|
61
|
+
if (!result.ok) {
|
|
62
|
+
// Log warning for debugging, but don't fail hard
|
|
63
|
+
if (result.reason !== "not-configured" && result.reason !== "not-found") {
|
|
64
|
+
console.error(`[axusage] Vault fetch failed for ${agentId}/${credentialName}: ${result.reason}`);
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const token = extractAccessToken(result.credentials);
|
|
69
|
+
if (!token) {
|
|
70
|
+
console.error(`[axusage] Vault credentials for ${agentId}/${credentialName} missing access token. ` +
|
|
71
|
+
`Credential type: ${result.credentials.type}`);
|
|
72
|
+
}
|
|
73
|
+
return token;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error(`[axusage] Vault fetch error for ${agentId}/${credentialName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Fetch access token from local credential store.
|
|
82
|
+
*
|
|
83
|
+
* @returns Access token string or undefined if not available
|
|
84
|
+
*/
|
|
85
|
+
async function fetchFromLocal(agentId) {
|
|
86
|
+
try {
|
|
87
|
+
return await getAgentAccessToken(agentId);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.error(`[axusage] Local credential fetch error for ${agentId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get access token for a service.
|
|
96
|
+
*
|
|
97
|
+
* Uses the configured credential source for the service:
|
|
98
|
+
* - "local": Fetch from local axauth credential store
|
|
99
|
+
* - "vault": Fetch from axvault server (requires credential name)
|
|
100
|
+
* - "auto": Try vault if configured and name provided, fallback to local
|
|
101
|
+
*
|
|
102
|
+
* @param service - Service ID (e.g., "claude", "chatgpt", "gemini")
|
|
103
|
+
* @returns Access token string or undefined if not available
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* const token = await getServiceAccessToken("claude");
|
|
107
|
+
* if (!token) {
|
|
108
|
+
* console.error("No credentials found for Claude");
|
|
109
|
+
* }
|
|
110
|
+
*/
|
|
111
|
+
async function getServiceAccessToken(service) {
|
|
112
|
+
const config = getServiceSourceConfig(service);
|
|
113
|
+
const agentId = SERVICE_TO_AGENT[service];
|
|
114
|
+
switch (config.source) {
|
|
115
|
+
case "local": {
|
|
116
|
+
return fetchFromLocal(agentId);
|
|
117
|
+
}
|
|
118
|
+
case "vault": {
|
|
119
|
+
if (!config.name) {
|
|
120
|
+
console.error(`[axusage] Vault source requires credential name for ${service}. ` +
|
|
121
|
+
`Set {"${service}": {"source": "vault", "name": "your-name"}} in config.`);
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const token = await fetchFromVault(agentId, config.name);
|
|
125
|
+
if (!token) {
|
|
126
|
+
// User explicitly selected vault but it failed - provide clear feedback
|
|
127
|
+
console.error(`[axusage] Vault credential fetch failed for ${service}. ` +
|
|
128
|
+
`Check that vault is configured (AXVAULT env) and credential "${config.name}" exists.`);
|
|
129
|
+
}
|
|
130
|
+
return token;
|
|
131
|
+
}
|
|
132
|
+
case "auto": {
|
|
133
|
+
// Auto mode: try vault first if configured and name provided
|
|
134
|
+
if (config.name && isVaultConfigured()) {
|
|
135
|
+
const vaultToken = await fetchFromVault(agentId, config.name);
|
|
136
|
+
if (vaultToken) {
|
|
137
|
+
return vaultToken;
|
|
138
|
+
}
|
|
139
|
+
// Fallback to local if vault failed
|
|
140
|
+
}
|
|
141
|
+
// No credential name or vault not configured: use local only
|
|
142
|
+
return fetchFromLocal(agentId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export { getServiceAccessToken };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BrowserContext } from "playwright";
|
|
2
2
|
/**
|
|
3
3
|
* Persist context storage state to disk with secure permissions (0o600).
|
|
4
|
-
* Errors are
|
|
4
|
+
* Errors are logged as warnings to avoid blocking the main operation.
|
|
5
5
|
*/
|
|
6
6
|
export declare function persistStorageState(context: BrowserContext, storagePath: string): Promise<void>;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeAtomicJson } from "../utils/write-atomic-json.js";
|
|
2
|
+
import { chalk } from "../utils/color.js";
|
|
2
3
|
/**
|
|
3
4
|
* Persist context storage state to disk with secure permissions (0o600).
|
|
4
|
-
* Errors are
|
|
5
|
+
* Errors are logged as warnings to avoid blocking the main operation.
|
|
5
6
|
*/
|
|
6
7
|
export async function persistStorageState(context, storagePath) {
|
|
7
8
|
try {
|
|
8
|
-
await context.storageState(
|
|
9
|
-
await
|
|
10
|
-
// best effort: permissions may already be correct or OS may ignore
|
|
11
|
-
});
|
|
9
|
+
const state = await context.storageState();
|
|
10
|
+
await writeAtomicJson(storagePath, state, 0o600);
|
|
12
11
|
}
|
|
13
|
-
catch {
|
|
14
|
-
|
|
12
|
+
catch (error) {
|
|
13
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
14
|
+
console.error(chalk.yellow(`Warning: Failed to persist auth state to ${storagePath} (${details}).`));
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import { getServiceAuthConfig } from "./service-auth-configs.js";
|
|
2
2
|
import { waitForLogin } from "./wait-for-login.js";
|
|
3
3
|
import { verifySessionByFetching } from "./verify-session.js";
|
|
4
|
-
import {
|
|
4
|
+
import { writeAtomicJson } from "../utils/write-atomic-json.js";
|
|
5
|
+
function describeLoginOutcome(outcome) {
|
|
6
|
+
switch (outcome) {
|
|
7
|
+
case "manual": {
|
|
8
|
+
return "after manual continuation";
|
|
9
|
+
}
|
|
10
|
+
case "timeout": {
|
|
11
|
+
return "after login timeout";
|
|
12
|
+
}
|
|
13
|
+
case "closed": {
|
|
14
|
+
return "after the browser window closed";
|
|
15
|
+
}
|
|
16
|
+
case "aborted": {
|
|
17
|
+
return "after prompt cancellation";
|
|
18
|
+
}
|
|
19
|
+
case "selector": {
|
|
20
|
+
return "after detecting a login signal";
|
|
21
|
+
}
|
|
22
|
+
case "skipped": {
|
|
23
|
+
return "without waiting for a login signal";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
5
27
|
export async function setupAuthInContext(service, context, storagePath) {
|
|
6
28
|
const page = await context.newPage();
|
|
7
29
|
try {
|
|
@@ -9,24 +31,27 @@ export async function setupAuthInContext(service, context, storagePath) {
|
|
|
9
31
|
await page.goto(config.url);
|
|
10
32
|
const selectors = config.waitForSelectors ??
|
|
11
33
|
(config.waitForSelector ? [config.waitForSelector] : []);
|
|
12
|
-
await waitForLoginForService(page, selectors);
|
|
34
|
+
const loginOutcome = await waitForLoginForService(page, selectors);
|
|
35
|
+
const outcomeLabel = describeLoginOutcome(loginOutcome);
|
|
36
|
+
if (loginOutcome === "aborted") {
|
|
37
|
+
throw new Error("Authentication was canceled. Authentication was not saved.");
|
|
38
|
+
}
|
|
13
39
|
if (config.verifyUrl) {
|
|
14
40
|
const ok = config.verifyFunction
|
|
15
41
|
? await config.verifyFunction(context, config.verifyUrl)
|
|
16
42
|
: await verifySessionByFetching(context, config.verifyUrl);
|
|
17
43
|
if (!ok) {
|
|
18
|
-
|
|
44
|
+
throw new Error(`Unable to verify session via ${config.verifyUrl} ${outcomeLabel}. Authentication was not saved. Ensure login completed successfully and retry.`);
|
|
19
45
|
}
|
|
20
46
|
}
|
|
47
|
+
else if (selectors.length > 0 && loginOutcome !== "selector") {
|
|
48
|
+
// Without a verification URL, we only persist when a login selector confirms success.
|
|
49
|
+
throw new Error(`Login was not confirmed ${outcomeLabel}. Authentication was not saved.`);
|
|
50
|
+
}
|
|
21
51
|
// Capture user agent for future headless contexts
|
|
22
52
|
const userAgent = await page.evaluate(() => navigator.userAgent);
|
|
23
|
-
await context.storageState(
|
|
24
|
-
|
|
25
|
-
await chmod(storagePath, 0o600);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// best effort to restrict sensitive storage state
|
|
29
|
-
}
|
|
53
|
+
const state = await context.storageState();
|
|
54
|
+
await writeAtomicJson(storagePath, state, 0o600);
|
|
30
55
|
return userAgent;
|
|
31
56
|
}
|
|
32
57
|
finally {
|
|
@@ -35,6 +60,8 @@ export async function setupAuthInContext(service, context, storagePath) {
|
|
|
35
60
|
}
|
|
36
61
|
async function waitForLoginForService(page, selectors) {
|
|
37
62
|
if (selectors.length > 0) {
|
|
38
|
-
|
|
63
|
+
return waitForLogin(page, selectors);
|
|
39
64
|
}
|
|
65
|
+
// When no selectors are configured, skip waiting and rely on verification if available.
|
|
66
|
+
return "skipped";
|
|
40
67
|
}
|
|
@@ -6,11 +6,13 @@ export const SUPPORTED_SERVICES = [
|
|
|
6
6
|
];
|
|
7
7
|
export function validateService(service) {
|
|
8
8
|
if (!service) {
|
|
9
|
-
throw new Error(`Service is required. Supported services: ${SUPPORTED_SERVICES.join(", ")}`
|
|
9
|
+
throw new Error(`Service is required. Supported services: ${SUPPORTED_SERVICES.join(", ")}. ` +
|
|
10
|
+
"Run 'axusage --help' for usage.");
|
|
10
11
|
}
|
|
11
12
|
const normalizedService = service.toLowerCase();
|
|
12
13
|
if (!SUPPORTED_SERVICES.includes(normalizedService)) {
|
|
13
|
-
throw new Error(`Unsupported service: ${service}. Supported services: ${SUPPORTED_SERVICES.join(", ")}`
|
|
14
|
+
throw new Error(`Unsupported service: ${service}. Supported services: ${SUPPORTED_SERVICES.join(", ")}. ` +
|
|
15
|
+
"Run 'axusage --help' for usage.");
|
|
14
16
|
}
|
|
15
17
|
return normalizedService;
|
|
16
18
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Page } from "playwright";
|
|
2
2
|
/**
|
|
3
3
|
* Waits until one of the selectors appears on the page, or the user presses Enter to continue.
|
|
4
4
|
*/
|
|
5
|
-
export
|
|
5
|
+
export type LoginWaitOutcome = "selector" | "manual" | "timeout" | "closed" | "aborted" | "skipped";
|
|
6
|
+
export declare function waitForLogin(page: Page, selectors: readonly string[]): Promise<LoginWaitOutcome>;
|