axusage 2.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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +254 -0
  3. package/bin/axusage +2 -0
  4. package/dist/adapters/chatgpt.d.ts +8 -0
  5. package/dist/adapters/chatgpt.js +68 -0
  6. package/dist/adapters/claude.d.ts +9 -0
  7. package/dist/adapters/claude.js +108 -0
  8. package/dist/adapters/coalesce-claude-usage-response.d.ts +12 -0
  9. package/dist/adapters/coalesce-claude-usage-response.js +119 -0
  10. package/dist/adapters/gemini.d.ts +8 -0
  11. package/dist/adapters/gemini.js +43 -0
  12. package/dist/adapters/github-copilot.d.ts +6 -0
  13. package/dist/adapters/github-copilot.js +56 -0
  14. package/dist/adapters/parse-chatgpt-usage.d.ts +15 -0
  15. package/dist/adapters/parse-chatgpt-usage.js +28 -0
  16. package/dist/adapters/parse-claude-usage.d.ts +16 -0
  17. package/dist/adapters/parse-claude-usage.js +75 -0
  18. package/dist/adapters/parse-gemini-usage.d.ts +55 -0
  19. package/dist/adapters/parse-gemini-usage.js +151 -0
  20. package/dist/adapters/parse-github-copilot-usage.d.ts +23 -0
  21. package/dist/adapters/parse-github-copilot-usage.js +78 -0
  22. package/dist/cli.d.ts +2 -0
  23. package/dist/cli.js +69 -0
  24. package/dist/commands/auth-clear-command.d.ts +5 -0
  25. package/dist/commands/auth-clear-command.js +25 -0
  26. package/dist/commands/auth-setup-command.d.ts +11 -0
  27. package/dist/commands/auth-setup-command.js +45 -0
  28. package/dist/commands/auth-status-command.d.ts +5 -0
  29. package/dist/commands/auth-status-command.js +25 -0
  30. package/dist/commands/fetch-service-usage-with-reauth.d.ts +7 -0
  31. package/dist/commands/fetch-service-usage-with-reauth.js +45 -0
  32. package/dist/commands/fetch-service-usage.d.ts +8 -0
  33. package/dist/commands/fetch-service-usage.js +19 -0
  34. package/dist/commands/run-auth-setup.d.ts +29 -0
  35. package/dist/commands/run-auth-setup.js +91 -0
  36. package/dist/commands/usage-command.d.ts +15 -0
  37. package/dist/commands/usage-command.js +146 -0
  38. package/dist/services/app-paths.d.ts +9 -0
  39. package/dist/services/app-paths.js +39 -0
  40. package/dist/services/auth-storage-path.d.ts +3 -0
  41. package/dist/services/auth-storage-path.js +7 -0
  42. package/dist/services/auth-timeouts.d.ts +4 -0
  43. package/dist/services/auth-timeouts.js +4 -0
  44. package/dist/services/browser-auth-manager.d.ts +49 -0
  45. package/dist/services/browser-auth-manager.js +113 -0
  46. package/dist/services/create-auth-context.d.ts +8 -0
  47. package/dist/services/create-auth-context.js +34 -0
  48. package/dist/services/do-setup-auth.d.ts +3 -0
  49. package/dist/services/do-setup-auth.js +25 -0
  50. package/dist/services/fetch-json-with-context.d.ts +5 -0
  51. package/dist/services/fetch-json-with-context.js +37 -0
  52. package/dist/services/gemini-api.d.ts +11 -0
  53. package/dist/services/gemini-api.js +109 -0
  54. package/dist/services/launch-chromium.d.ts +6 -0
  55. package/dist/services/launch-chromium.js +20 -0
  56. package/dist/services/persist-storage-state.d.ts +6 -0
  57. package/dist/services/persist-storage-state.js +16 -0
  58. package/dist/services/request-service.d.ts +3 -0
  59. package/dist/services/request-service.js +4 -0
  60. package/dist/services/service-adapter-registry.d.ts +18 -0
  61. package/dist/services/service-adapter-registry.js +26 -0
  62. package/dist/services/service-auth-configs.d.ts +15 -0
  63. package/dist/services/service-auth-configs.js +26 -0
  64. package/dist/services/setup-auth-flow.d.ts +3 -0
  65. package/dist/services/setup-auth-flow.js +40 -0
  66. package/dist/services/shared-browser-auth-manager.d.ts +4 -0
  67. package/dist/services/shared-browser-auth-manager.js +80 -0
  68. package/dist/services/supported-service.d.ts +6 -0
  69. package/dist/services/supported-service.js +16 -0
  70. package/dist/services/verify-session.d.ts +2 -0
  71. package/dist/services/verify-session.js +25 -0
  72. package/dist/services/wait-for-login.d.ts +5 -0
  73. package/dist/services/wait-for-login.js +44 -0
  74. package/dist/types/chatgpt.d.ts +32 -0
  75. package/dist/types/chatgpt.js +21 -0
  76. package/dist/types/domain.d.ts +57 -0
  77. package/dist/types/domain.js +16 -0
  78. package/dist/types/gemini.d.ts +31 -0
  79. package/dist/types/gemini.js +27 -0
  80. package/dist/types/github-copilot.d.ts +21 -0
  81. package/dist/types/github-copilot.js +27 -0
  82. package/dist/types/usage.d.ts +31 -0
  83. package/dist/types/usage.js +25 -0
  84. package/dist/utils/calculate-usage-rate.d.ts +9 -0
  85. package/dist/utils/calculate-usage-rate.js +31 -0
  86. package/dist/utils/classify-usage-rate.d.ts +6 -0
  87. package/dist/utils/classify-usage-rate.js +10 -0
  88. package/dist/utils/format-prometheus-metrics.d.ts +6 -0
  89. package/dist/utils/format-prometheus-metrics.js +20 -0
  90. package/dist/utils/format-service-usage.d.ts +18 -0
  91. package/dist/utils/format-service-usage.js +120 -0
  92. package/package.json +83 -0
@@ -0,0 +1,25 @@
1
+ import chalk from "chalk";
2
+ import { existsSync } from "node:fs";
3
+ import { SUPPORTED_SERVICES, validateService, } from "../services/supported-service.js";
4
+ import { getStorageStatePathFor } from "../services/auth-storage-path.js";
5
+ import { getBrowserContextsDirectory } from "../services/app-paths.js";
6
+ export function authStatusCommand(options) {
7
+ const servicesToCheck = options.service
8
+ ? [validateService(options.service)]
9
+ : SUPPORTED_SERVICES;
10
+ const dataDirectory = getBrowserContextsDirectory();
11
+ console.log(chalk.blue("\nAuthentication Status:\n"));
12
+ for (const service of servicesToCheck) {
13
+ const storagePath = getStorageStatePathFor(dataDirectory, service);
14
+ const hasAuth = existsSync(storagePath);
15
+ const status = hasAuth
16
+ ? chalk.green("✓ Authenticated")
17
+ : chalk.gray("✗ Not authenticated");
18
+ console.log(`${chalk.bold(service)}: ${status}`);
19
+ console.log(` ${chalk.dim("Storage:")} ${chalk.dim(storagePath)}`);
20
+ }
21
+ const allAuthenticated = servicesToCheck.every((s) => existsSync(getStorageStatePathFor(dataDirectory, s)));
22
+ if (!allAuthenticated) {
23
+ console.error(chalk.gray(`\nTo set up authentication, run: ${chalk.cyan("axusage auth setup <service>")}`));
24
+ }
25
+ }
@@ -0,0 +1,7 @@
1
+ import type { ServiceResult } from "../types/domain.js";
2
+ /**
3
+ * Fetch usage for a service, with automatic re-authentication on auth errors.
4
+ * Prompts the user to re-authenticate if the initial fetch fails with an auth error,
5
+ * then retries the fetch. Returns the original result if re-authentication fails.
6
+ */
7
+ export declare function fetchServiceUsageWithAutoReauth(serviceName: string, interactive: boolean): Promise<ServiceResult>;
@@ -0,0 +1,45 @@
1
+ import chalk from "chalk";
2
+ import { fetchServiceUsage } from "./fetch-service-usage.js";
3
+ import { isAuthFailure, runAuthSetup } from "./run-auth-setup.js";
4
+ import { validateService } from "../services/supported-service.js";
5
+ /**
6
+ * Fetch usage for a service, with automatic re-authentication on auth errors.
7
+ * Prompts the user to re-authenticate if the initial fetch fails with an auth error,
8
+ * then retries the fetch. Returns the original result if re-authentication fails.
9
+ */
10
+ export async function fetchServiceUsageWithAutoReauth(serviceName, interactive) {
11
+ const result = await fetchServiceUsage(serviceName);
12
+ if (!interactive) {
13
+ return { service: serviceName, result };
14
+ }
15
+ // If auth error, try to re-authenticate and retry
16
+ if (isAuthFailure(result)) {
17
+ console.error(chalk.yellow(`⚠ Authentication failed for ${serviceName}. Opening browser to re-authenticate...`));
18
+ try {
19
+ const service = validateService(serviceName);
20
+ const authSuccess = await runAuthSetup(service);
21
+ if (authSuccess) {
22
+ if (process.stderr.isTTY) {
23
+ console.error(chalk.blue(`Retrying ${serviceName} usage fetch...\n`));
24
+ }
25
+ const retryResult = await fetchServiceUsage(serviceName);
26
+ return { service: serviceName, result: retryResult };
27
+ }
28
+ else {
29
+ console.error(chalk.red(`Re-authentication failed for ${serviceName}.`));
30
+ }
31
+ }
32
+ catch (error) {
33
+ const errorMessage = error instanceof Error ? error.message : String(error);
34
+ // Distinguish validation errors from auth setup errors
35
+ const isValidationError = errorMessage.includes("Unsupported service") ||
36
+ errorMessage.includes("Service is required");
37
+ const prefix = isValidationError
38
+ ? "Invalid service"
39
+ : "Failed to re-authenticate";
40
+ console.error(chalk.red(`${prefix}: ${errorMessage}`));
41
+ }
42
+ }
43
+ // Return original result if re-authentication failed or was not attempted
44
+ return { service: serviceName, result };
45
+ }
@@ -0,0 +1,8 @@
1
+ import type { ApiError, Result, ServiceUsageData } from "../types/domain.js";
2
+ export type UsageCommandOptions = {
3
+ readonly service?: string;
4
+ readonly format?: "text" | "tsv" | "json" | "prometheus";
5
+ readonly interactive?: boolean;
6
+ };
7
+ export declare function selectServicesToQuery(service?: string): string[];
8
+ export declare function fetchServiceUsage(serviceName: string): Promise<Result<ServiceUsageData, ApiError>>;
@@ -0,0 +1,19 @@
1
+ import { ApiError as ApiErrorClass } from "../types/domain.js";
2
+ import { getServiceAdapter } from "../services/service-adapter-registry.js";
3
+ const ALL_SERVICES = ["claude", "chatgpt", "github-copilot", "gemini"];
4
+ export function selectServicesToQuery(service) {
5
+ const normalized = service?.toLowerCase();
6
+ if (!service || normalized === "all")
7
+ return [...ALL_SERVICES];
8
+ return [service];
9
+ }
10
+ export async function fetchServiceUsage(serviceName) {
11
+ const adapter = getServiceAdapter(serviceName);
12
+ if (!adapter) {
13
+ return {
14
+ ok: false,
15
+ error: new ApiErrorClass(`Unknown service "${serviceName}"`),
16
+ };
17
+ }
18
+ return await adapter.fetchUsage();
19
+ }
@@ -0,0 +1,29 @@
1
+ import type { SupportedService } from "../services/supported-service.js";
2
+ import type { ApiError, Result, ServiceUsageData } from "../types/domain.js";
3
+ /**
4
+ * Check if an error message indicates an authentication issue.
5
+ * Matches common authentication error patterns like "unauthorized", "401",
6
+ * "authentication failed", etc. with word boundaries to avoid false positives.
7
+ *
8
+ * @param message - The error message to check
9
+ * @returns true if the message indicates an authentication error
10
+ *
11
+ * @example
12
+ * isAuthError("401 Unauthorized") // true
13
+ * isAuthError("Network timeout") // false
14
+ */
15
+ export declare function isAuthError(message: string): boolean;
16
+ /**
17
+ * Check if a fetch result indicates an authentication failure.
18
+ * Combines the result error check with auth error pattern matching.
19
+ */
20
+ export declare function isAuthFailure(result: Result<ServiceUsageData, ApiError>): boolean;
21
+ /**
22
+ * Run auth setup for a service programmatically.
23
+ * Returns true if auth setup completed successfully.
24
+ * Times out after 5 minutes to prevent indefinite hangs.
25
+ *
26
+ * Note: Gemini uses CLI-based auth and cannot use browser-based re-auth.
27
+ * This function prints instructions and returns false for Gemini.
28
+ */
29
+ export declare function runAuthSetup(service: SupportedService): Promise<boolean>;
@@ -0,0 +1,91 @@
1
+ import chalk from "chalk";
2
+ import { BrowserAuthManager } from "../services/browser-auth-manager.js";
3
+ /** Timeout for authentication setup (5 minutes) */
4
+ const AUTH_SETUP_TIMEOUT_MS = 300_000;
5
+ /**
6
+ * Check if an error message indicates an authentication issue.
7
+ * Matches common authentication error patterns like "unauthorized", "401",
8
+ * "authentication failed", etc. with word boundaries to avoid false positives.
9
+ *
10
+ * @param message - The error message to check
11
+ * @returns true if the message indicates an authentication error
12
+ *
13
+ * @example
14
+ * isAuthError("401 Unauthorized") // true
15
+ * isAuthError("Network timeout") // false
16
+ */
17
+ export function isAuthError(message) {
18
+ const authPatterns = [
19
+ /\bauthentication\s+failed\b/iu,
20
+ /\bno\s+saved\s+authentication\b/iu,
21
+ /\b401\b/u,
22
+ /\bunauthorized\b/iu,
23
+ /\bsession\s+expired\b/iu,
24
+ /\blogin\s+required\b/iu,
25
+ /\bcredentials?\s+(expired|invalid)\b/iu,
26
+ ];
27
+ return authPatterns.some((pattern) => pattern.test(message));
28
+ }
29
+ /**
30
+ * Check if a fetch result indicates an authentication failure.
31
+ * Combines the result error check with auth error pattern matching.
32
+ */
33
+ export function isAuthFailure(result) {
34
+ return (!result.ok &&
35
+ Boolean(result.error.message) &&
36
+ isAuthError(result.error.message));
37
+ }
38
+ /**
39
+ * Run auth setup for a service programmatically.
40
+ * Returns true if auth setup completed successfully.
41
+ * Times out after 5 minutes to prevent indefinite hangs.
42
+ *
43
+ * Note: Gemini uses CLI-based auth and cannot use browser-based re-auth.
44
+ * This function prints instructions and returns false for Gemini.
45
+ */
46
+ export async function runAuthSetup(service) {
47
+ // CLI-based auth cannot use browser auth flow
48
+ if (service === "gemini") {
49
+ console.error(chalk.yellow("\nGemini uses CLI-based authentication managed by the Gemini CLI."));
50
+ console.error(chalk.gray("\nTo re-authenticate, run:"));
51
+ console.error(chalk.cyan(" gemini"));
52
+ console.error(chalk.gray("\nThe Gemini CLI will guide you through the OAuth login process.\n"));
53
+ return false;
54
+ }
55
+ if (service === "claude") {
56
+ console.error(chalk.yellow("\nClaude uses CLI-based authentication managed by Claude Code."));
57
+ console.error(chalk.gray("\nTo re-authenticate, run:"));
58
+ console.error(chalk.cyan(" claude"));
59
+ console.error(chalk.gray("\nClaude Code will guide you through authentication.\n"));
60
+ return false;
61
+ }
62
+ if (service === "chatgpt") {
63
+ console.error(chalk.yellow("\nChatGPT uses CLI-based authentication managed by Codex."));
64
+ console.error(chalk.gray("\nTo re-authenticate, run:"));
65
+ console.error(chalk.cyan(" codex"));
66
+ console.error(chalk.gray("\nCodex will guide you through authentication.\n"));
67
+ return false;
68
+ }
69
+ const manager = new BrowserAuthManager({ headless: false });
70
+ let timeoutId;
71
+ try {
72
+ console.error(chalk.blue(`\nOpening browser for ${service} authentication...\n`));
73
+ const setupPromise = manager.setupAuth(service);
74
+ const timeoutPromise = new Promise((_, reject) => {
75
+ timeoutId = setTimeout(() => {
76
+ reject(new Error("Authentication setup timed out after 5 minutes"));
77
+ }, AUTH_SETUP_TIMEOUT_MS);
78
+ });
79
+ await Promise.race([setupPromise, timeoutPromise]);
80
+ console.error(chalk.green(`\n✓ Authentication for ${service} is complete!\n`));
81
+ return true;
82
+ }
83
+ catch (error) {
84
+ console.error(chalk.red(`\n✗ Failed to set up authentication: ${error instanceof Error ? error.message : String(error)}`));
85
+ return false;
86
+ }
87
+ finally {
88
+ clearTimeout(timeoutId);
89
+ await manager.close();
90
+ }
91
+ }
@@ -0,0 +1,15 @@
1
+ import type { ServiceResult } from "../types/domain.js";
2
+ import type { UsageCommandOptions } from "./fetch-service-usage.js";
3
+ /**
4
+ * Fetches usage for services using hybrid strategy:
5
+ * 1. Try all services in parallel first (fast path for valid credentials)
6
+ * 2. If any service fails with auth error, retry those sequentially with re-auth
7
+ *
8
+ * This maintains ~2s response time when credentials are valid while gracefully
9
+ * handling authentication failures that require interactive prompts.
10
+ */
11
+ export declare function fetchServicesWithHybridStrategy(servicesToQuery: string[], interactive: boolean): Promise<ServiceResult[]>;
12
+ /**
13
+ * Executes the usage command
14
+ */
15
+ export declare function usageCommand(options: UsageCommandOptions): Promise<void>;
@@ -0,0 +1,146 @@
1
+ import chalk from "chalk";
2
+ import { formatServiceUsageData, formatServiceUsageDataAsJson, formatServiceUsageAsTsv, toJsonObject, } from "../utils/format-service-usage.js";
3
+ import { formatPrometheusMetrics } from "../utils/format-prometheus-metrics.js";
4
+ import { fetchServiceUsage, selectServicesToQuery, } from "./fetch-service-usage.js";
5
+ import { fetchServiceUsageWithAutoReauth } from "./fetch-service-usage-with-reauth.js";
6
+ import { isAuthFailure } from "./run-auth-setup.js";
7
+ /**
8
+ * Fetches usage for services using hybrid strategy:
9
+ * 1. Try all services in parallel first (fast path for valid credentials)
10
+ * 2. If any service fails with auth error, retry those sequentially with re-auth
11
+ *
12
+ * This maintains ~2s response time when credentials are valid while gracefully
13
+ * handling authentication failures that require interactive prompts.
14
+ */
15
+ export async function fetchServicesWithHybridStrategy(servicesToQuery, interactive) {
16
+ // First attempt: fetch all services in parallel
17
+ const parallelResults = await Promise.all(servicesToQuery.map(async (serviceName) => {
18
+ const result = await fetchServiceUsage(serviceName);
19
+ return { service: serviceName, result };
20
+ }));
21
+ // Check for auth errors
22
+ const authFailures = parallelResults.filter(({ result }) => isAuthFailure(result));
23
+ // If no auth failures, return parallel results
24
+ if (authFailures.length === 0 || !interactive) {
25
+ return parallelResults;
26
+ }
27
+ const shouldShowProgress = process.stderr.isTTY;
28
+ // Retry auth failures sequentially with re-authentication
29
+ const retryResults = [];
30
+ for (const [index, { service }] of authFailures.entries()) {
31
+ if (shouldShowProgress) {
32
+ console.error(chalk.dim(`[${String(index + 1)}/${String(authFailures.length)}] Re-authenticating ${service}...`));
33
+ }
34
+ const result = await fetchServiceUsageWithAutoReauth(service, interactive);
35
+ retryResults.push(result);
36
+ }
37
+ if (shouldShowProgress) {
38
+ console.error(chalk.green(`✓ Completed ${String(retryResults.length)} re-authentication${retryResults.length === 1 ? "" : "s"}\n`));
39
+ }
40
+ // Merge results: keep successful parallel results, replace auth failures with retries
41
+ // Build a map for O(1) lookups instead of O(n²) find() calls
42
+ const retryMap = new Map(retryResults.map((r) => [r.service, r]));
43
+ return parallelResults.map((parallelResult) => retryMap.get(parallelResult.service) ?? parallelResult);
44
+ }
45
+ /**
46
+ * Executes the usage command
47
+ */
48
+ export async function usageCommand(options) {
49
+ const servicesToQuery = selectServicesToQuery(options.service);
50
+ const interactive = options.interactive ?? false;
51
+ // Fetch usage data using hybrid parallel/sequential strategy
52
+ const results = await fetchServicesWithHybridStrategy(servicesToQuery, interactive);
53
+ // Collect successful results and errors
54
+ const successes = [];
55
+ const errors = [];
56
+ const authFailureServices = new Set();
57
+ for (const { service, result } of results) {
58
+ if (result.ok) {
59
+ successes.push(result.value);
60
+ }
61
+ else {
62
+ errors.push({ service, error: result.error });
63
+ if (isAuthFailure(result))
64
+ authFailureServices.add(service);
65
+ }
66
+ }
67
+ // Display errors if any (but don't exit if we have at least one success)
68
+ if (errors.length > 0) {
69
+ for (const { service, error } of errors) {
70
+ console.error(chalk.yellow(`⚠ Warning: Failed to fetch ${service} usage:`));
71
+ console.error(chalk.gray(` ${error.message}`));
72
+ if (error.status) {
73
+ console.error(chalk.gray(` Status: ${String(error.status)}`));
74
+ }
75
+ }
76
+ if (successes.length > 0) {
77
+ console.error(); // Empty line for spacing
78
+ }
79
+ }
80
+ if (!interactive && authFailureServices.size > 0) {
81
+ const list = [...authFailureServices].join(", ");
82
+ console.error(chalk.gray(`Authentication required for: ${list}. Run 'axusage auth setup <service>' or re-run with '--interactive' to re-authenticate during fetch.`));
83
+ if (successes.length > 0) {
84
+ console.error();
85
+ }
86
+ }
87
+ // Exit if no services succeeded
88
+ if (successes.length === 0) {
89
+ console.error(chalk.red("\nNo services could be queried successfully."));
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+ // Display results
94
+ const hasPartialFailures = errors.length > 0;
95
+ const format = options.format ?? "text";
96
+ switch (format) {
97
+ case "tsv": {
98
+ console.log(formatServiceUsageAsTsv(successes));
99
+ break;
100
+ }
101
+ case "json": {
102
+ const [singleSuccess] = successes;
103
+ if (successes.length === 1 && !hasPartialFailures && singleSuccess) {
104
+ console.log(formatServiceUsageDataAsJson(singleSuccess));
105
+ }
106
+ else {
107
+ const payload = successes.length === 1 && singleSuccess
108
+ ? toJsonObject(singleSuccess)
109
+ : successes.map((data) => toJsonObject(data));
110
+ const output = hasPartialFailures
111
+ ? {
112
+ results: payload,
113
+ errors: errors.map(({ service, error }) => ({
114
+ service,
115
+ message: error.message,
116
+ status: error.status,
117
+ })),
118
+ }
119
+ : payload;
120
+ // eslint-disable-next-line unicorn/no-null -- JSON.stringify requires null for no replacer
121
+ console.log(JSON.stringify(output, null, 2));
122
+ }
123
+ break;
124
+ }
125
+ case "prometheus": {
126
+ // Emit Prometheus text metrics using prom-client
127
+ const output = await formatPrometheusMetrics(successes);
128
+ process.stdout.write(output);
129
+ break;
130
+ }
131
+ case "text": {
132
+ // For human-readable output, display each service's results
133
+ for (const data of successes) {
134
+ const index = successes.indexOf(data);
135
+ if (index > 0) {
136
+ console.log(); // Add spacing between services
137
+ }
138
+ console.log(formatServiceUsageData(data));
139
+ }
140
+ break;
141
+ }
142
+ }
143
+ if (hasPartialFailures) {
144
+ process.exitCode = 2;
145
+ }
146
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Directory for storing browser authentication contexts
3
+ */
4
+ export declare function getBrowserContextsDirectory(): string;
5
+ /**
6
+ * Ensure a directory exists with restricted permissions (owner-only access).
7
+ * Creates the directory recursively if needed and sets mode 0o700.
8
+ */
9
+ export declare function ensureSecureDirectory(directoryPath: string): Promise<void>;
@@ -0,0 +1,39 @@
1
+ import envPaths from "env-paths";
2
+ import path from "node:path";
3
+ import { mkdir, chmod } from "node:fs/promises";
4
+ /**
5
+ * env-paths resolves directories during module initialization, so changes to
6
+ * environment variables (like XDG_DATA_HOME) after the first import will not
7
+ * be picked up without restarting the process.
8
+ */
9
+ const paths = envPaths("axusage", { suffix: "" });
10
+ /**
11
+ * Directory for storing browser authentication contexts
12
+ */
13
+ export function getBrowserContextsDirectory() {
14
+ return path.join(paths.data, "browser-contexts");
15
+ }
16
+ /**
17
+ * Ensure a directory exists with restricted permissions (owner-only access).
18
+ * Creates the directory recursively if needed and sets mode 0o700.
19
+ */
20
+ export async function ensureSecureDirectory(directoryPath) {
21
+ try {
22
+ await mkdir(directoryPath, { recursive: true, mode: 0o700 });
23
+ }
24
+ catch (error) {
25
+ // Only ignore EEXIST; re-throw everything else
26
+ // mkdir may ignore mode due to umask; we'll enforce via chmod below
27
+ const isEexist = error instanceof Error && "code" in error && error.code === "EEXIST";
28
+ if (!isEexist) {
29
+ throw error;
30
+ }
31
+ }
32
+ try {
33
+ await chmod(directoryPath, 0o700);
34
+ }
35
+ catch {
36
+ // Best effort: some filesystems (network mounts, containers) may not
37
+ // support chmod or have different permission models
38
+ }
39
+ }
@@ -0,0 +1,3 @@
1
+ import type { SupportedService } from "./supported-service.js";
2
+ export declare function getStorageStatePathFor(dataDirectory: string, service: SupportedService): string;
3
+ export declare function getAuthMetaPathFor(dataDirectory: string, service: SupportedService): string;
@@ -0,0 +1,7 @@
1
+ import path from "node:path";
2
+ export function getStorageStatePathFor(dataDirectory, service) {
3
+ return path.join(dataDirectory, `${service}-auth.json`);
4
+ }
5
+ export function getAuthMetaPathFor(dataDirectory, service) {
6
+ return path.join(dataDirectory, `${service}-auth.meta.json`);
7
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Shared authentication-related timeouts.
3
+ */
4
+ export declare const LOGIN_TIMEOUT_MS = 300000;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Shared authentication-related timeouts.
3
+ */
4
+ export const LOGIN_TIMEOUT_MS = 300_000; // 5 minutes
@@ -0,0 +1,49 @@
1
+ import type { BrowserContext } from "playwright";
2
+ import type { SupportedService } from "./supported-service.js";
3
+ /**
4
+ * Configuration for browser authentication manager
5
+ */
6
+ type BrowserAuthConfig = {
7
+ readonly dataDir?: string;
8
+ readonly headless?: boolean;
9
+ };
10
+ /**
11
+ * Manages browser contexts for persistent authentication across services
12
+ */
13
+ export declare class BrowserAuthManager {
14
+ private readonly dataDir;
15
+ private readonly headless;
16
+ private browser;
17
+ private browserPromise;
18
+ constructor(config?: BrowserAuthConfig);
19
+ /**
20
+ * Get the storage state file path for a service
21
+ */
22
+ private getStorageStatePath;
23
+ /**
24
+ * Check if a service has saved authentication
25
+ */
26
+ hasAuth(service: SupportedService): boolean;
27
+ /**
28
+ * Ensure a Chromium browser instance is available
29
+ */
30
+ private ensureBrowser;
31
+ private launchAndStoreBrowser;
32
+ /**
33
+ * Set up authentication for a service by launching a browser for the user to log in
34
+ */
35
+ setupAuth(service: SupportedService): Promise<void>;
36
+ /**
37
+ * Get a browser context with saved authentication for a service
38
+ */
39
+ getAuthContext(service: SupportedService): Promise<BrowserContext>;
40
+ /**
41
+ * Make an authenticated request to a URL using the browser context
42
+ */
43
+ makeAuthenticatedRequest(service: SupportedService, url: string): Promise<string>;
44
+ /**
45
+ * Close the browser and clean up resources
46
+ */
47
+ close(): Promise<void>;
48
+ }
49
+ export {};
@@ -0,0 +1,113 @@
1
+ import { existsSync } from "node:fs";
2
+ import { getServiceAuthConfig } from "./service-auth-configs.js";
3
+ import { launchChromium } from "./launch-chromium.js";
4
+ import { requestService } from "./request-service.js";
5
+ import { doSetupAuth } from "./do-setup-auth.js";
6
+ import { getStorageStatePathFor } from "./auth-storage-path.js";
7
+ import { createAuthContext, loadStoredUserAgent, } from "./create-auth-context.js";
8
+ import { getBrowserContextsDirectory, ensureSecureDirectory, } from "./app-paths.js";
9
+ import { persistStorageState } from "./persist-storage-state.js";
10
+ /**
11
+ * Manages browser contexts for persistent authentication across services
12
+ */
13
+ export class BrowserAuthManager {
14
+ dataDir;
15
+ headless;
16
+ browser = undefined;
17
+ browserPromise = undefined;
18
+ constructor(config = {}) {
19
+ this.dataDir = config.dataDir || getBrowserContextsDirectory();
20
+ // Default to headless for non-interactive usage flows; auth setup passes headless: false
21
+ this.headless = config.headless ?? true;
22
+ }
23
+ /**
24
+ * Get the storage state file path for a service
25
+ */
26
+ getStorageStatePath(service) {
27
+ return getStorageStatePathFor(this.dataDir, service);
28
+ }
29
+ /**
30
+ * Check if a service has saved authentication
31
+ */
32
+ hasAuth(service) {
33
+ return existsSync(this.getStorageStatePath(service));
34
+ }
35
+ /**
36
+ * Ensure a Chromium browser instance is available
37
+ */
38
+ async ensureBrowser() {
39
+ if (!this.browserPromise) {
40
+ this.browserPromise = this.launchAndStoreBrowser();
41
+ }
42
+ return this.browserPromise;
43
+ }
44
+ async launchAndStoreBrowser() {
45
+ try {
46
+ const browser = await launchChromium(this.headless);
47
+ this.browser = browser;
48
+ return browser;
49
+ }
50
+ catch (error) {
51
+ // Allow retries on subsequent calls if the launch fails
52
+ this.browserPromise = undefined;
53
+ throw error;
54
+ }
55
+ }
56
+ /**
57
+ * Set up authentication for a service by launching a browser for the user to log in
58
+ */
59
+ async setupAuth(service) {
60
+ const config = getServiceAuthConfig(service);
61
+ await ensureSecureDirectory(this.dataDir);
62
+ const browser = await this.ensureBrowser();
63
+ const storagePath = this.getStorageStatePath(service);
64
+ // Load existing storage state if available - this gives the browser a chance
65
+ // to refresh expired cookies/tokens during the login flow
66
+ const storageState = existsSync(storagePath) ? storagePath : undefined;
67
+ const userAgent = await loadStoredUserAgent(this.dataDir, service);
68
+ let context;
69
+ try {
70
+ context = await browser.newContext({ storageState, userAgent });
71
+ }
72
+ catch {
73
+ // Corrupted storage state - fall back to fresh context
74
+ context = await browser.newContext({ userAgent });
75
+ }
76
+ try {
77
+ await doSetupAuth(service, context, this.getStorageStatePath(service), config.instructions);
78
+ }
79
+ finally {
80
+ await context.close();
81
+ }
82
+ }
83
+ /**
84
+ * Get a browser context with saved authentication for a service
85
+ */
86
+ async getAuthContext(service) {
87
+ const browser = await this.ensureBrowser();
88
+ return createAuthContext(browser, this.dataDir, service);
89
+ }
90
+ /**
91
+ * Make an authenticated request to a URL using the browser context
92
+ */
93
+ async makeAuthenticatedRequest(service, url) {
94
+ const context = await this.getAuthContext(service);
95
+ try {
96
+ return await requestService(service, url, () => Promise.resolve(context));
97
+ }
98
+ finally {
99
+ await persistStorageState(context, this.getStorageStatePath(service));
100
+ await context.close();
101
+ }
102
+ }
103
+ /**
104
+ * Close the browser and clean up resources
105
+ */
106
+ async close() {
107
+ if (this.browser) {
108
+ await this.browser.close();
109
+ this.browser = undefined;
110
+ this.browserPromise = undefined;
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,8 @@
1
+ import type { Browser, BrowserContext } from "playwright";
2
+ import type { SupportedService } from "./supported-service.js";
3
+ /**
4
+ * Load the stored userAgent from the auth meta file for a service.
5
+ * Returns undefined if meta file doesn't exist or is invalid.
6
+ */
7
+ export declare function loadStoredUserAgent(dataDirectory: string, service: SupportedService): Promise<string | undefined>;
8
+ export declare function createAuthContext(browser: Browser, dataDirectory: string, service: SupportedService): Promise<BrowserContext>;