hyper-agent-browser 0.1.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.
@@ -0,0 +1,97 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import type { Session } from "../session/store";
5
+
6
+ /**
7
+ * Stores and retrieves element reference mappings for a session
8
+ * Maps @eN references to actual CSS selectors
9
+ */
10
+ export class ReferenceStore {
11
+ private sessionDir: string;
12
+ private mappingsFile: string;
13
+ private mappings: Map<string, string> = new Map();
14
+
15
+ constructor(session: Session) {
16
+ this.sessionDir = join(session.userDataDir, "..");
17
+ this.mappingsFile = join(this.sessionDir, "element-mappings.json");
18
+ }
19
+
20
+ /**
21
+ * Load mappings from disk
22
+ */
23
+ async load(): Promise<void> {
24
+ if (!existsSync(this.mappingsFile)) {
25
+ this.mappings.clear();
26
+ return;
27
+ }
28
+
29
+ try {
30
+ const content = await readFile(this.mappingsFile, "utf-8");
31
+ const data = JSON.parse(content);
32
+ this.mappings = new Map(Object.entries(data));
33
+ } catch (error) {
34
+ console.error("Failed to load element mappings:", error);
35
+ this.mappings.clear();
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Save mappings to disk
41
+ */
42
+ async save(): Promise<void> {
43
+ try {
44
+ // Ensure directory exists
45
+ if (!existsSync(this.sessionDir)) {
46
+ await mkdir(this.sessionDir, { recursive: true, mode: 0o700 });
47
+ }
48
+
49
+ const data = Object.fromEntries(this.mappings);
50
+ await writeFile(this.mappingsFile, JSON.stringify(data, null, 2), "utf-8");
51
+ } catch (error) {
52
+ console.error("Failed to save element mappings:", error);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Store a new mapping
58
+ */
59
+ set(ref: string, selector: string): void {
60
+ this.mappings.set(ref, selector);
61
+ }
62
+
63
+ /**
64
+ * Get selector for a reference
65
+ */
66
+ get(ref: string): string | undefined {
67
+ return this.mappings.get(ref);
68
+ }
69
+
70
+ /**
71
+ * Update all mappings at once
72
+ */
73
+ setAll(mappings: Map<string, string>): void {
74
+ this.mappings = new Map(mappings);
75
+ }
76
+
77
+ /**
78
+ * Clear all mappings
79
+ */
80
+ clear(): void {
81
+ this.mappings.clear();
82
+ }
83
+
84
+ /**
85
+ * Get all mappings
86
+ */
87
+ getAll(): Map<string, string> {
88
+ return new Map(this.mappings);
89
+ }
90
+
91
+ /**
92
+ * Check if a reference exists
93
+ */
94
+ has(ref: string): boolean {
95
+ return this.mappings.has(ref);
96
+ }
97
+ }
@@ -0,0 +1,183 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { z } from "zod";
6
+
7
+ export const ConfigSchema = z.object({
8
+ version: z.string(),
9
+ defaults: z.object({
10
+ session: z.string(),
11
+ headed: z.boolean(),
12
+ channel: z.enum(["chrome", "msedge", "chromium"]),
13
+ timeout: z.number(),
14
+ }),
15
+ sessions: z
16
+ .object({
17
+ dataDir: z.string(),
18
+ })
19
+ .optional(),
20
+ browser: z
21
+ .object({
22
+ args: z.array(z.string()).optional(),
23
+ ignoreDefaultArgs: z.array(z.string()).optional(),
24
+ })
25
+ .optional(),
26
+ snapshot: z
27
+ .object({
28
+ interactiveRoles: z.array(z.string()).optional(),
29
+ })
30
+ .optional(),
31
+ });
32
+
33
+ export type Config = z.infer<typeof ConfigSchema>;
34
+
35
+ const DEFAULT_CONFIG: Config = {
36
+ version: "1.0",
37
+ defaults: {
38
+ session: "default",
39
+ headed: false,
40
+ channel: "chrome",
41
+ timeout: 30000,
42
+ },
43
+ sessions: {
44
+ dataDir: join(homedir(), ".hab", "sessions"),
45
+ },
46
+ browser: {
47
+ args: ["--disable-blink-features=AutomationControlled"],
48
+ ignoreDefaultArgs: ["--enable-automation"],
49
+ },
50
+ snapshot: {
51
+ interactiveRoles: [
52
+ "button",
53
+ "link",
54
+ "textbox",
55
+ "checkbox",
56
+ "radio",
57
+ "combobox",
58
+ "menuitem",
59
+ "tab",
60
+ ],
61
+ },
62
+ };
63
+
64
+ export function getConfigPath(): string {
65
+ const envPath = process.env.HAB_CONFIG;
66
+ if (envPath) {
67
+ return envPath;
68
+ }
69
+ return join(homedir(), ".hab", "config.json");
70
+ }
71
+
72
+ export async function loadConfig(): Promise<Config> {
73
+ const configPath = getConfigPath();
74
+
75
+ if (!existsSync(configPath)) {
76
+ return DEFAULT_CONFIG;
77
+ }
78
+
79
+ try {
80
+ const content = await readFile(configPath, "utf-8");
81
+ const data = JSON.parse(content);
82
+ const validated = ConfigSchema.parse(data);
83
+ return validated;
84
+ } catch (error) {
85
+ console.error(`Warning: Failed to load config from ${configPath}:`, error);
86
+ return DEFAULT_CONFIG;
87
+ }
88
+ }
89
+
90
+ export async function saveConfig(config: Config): Promise<void> {
91
+ const configPath = getConfigPath();
92
+ const configDir = join(configPath, "..");
93
+
94
+ // Ensure directory exists
95
+ if (!existsSync(configDir)) {
96
+ await mkdir(configDir, { recursive: true, mode: 0o700 });
97
+ }
98
+
99
+ await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
100
+
101
+ // Security: Set file permissions to 0o600 (owner read/write only)
102
+ // Protects sensitive configuration from other users
103
+ const { chmod } = await import("node:fs/promises");
104
+ await chmod(configPath, 0o600);
105
+ }
106
+
107
+ export async function getConfigValue(key: string): Promise<unknown> {
108
+ const config = await loadConfig();
109
+ const keys = key.split(".");
110
+
111
+ let value: any = config;
112
+ for (const k of keys) {
113
+ if (value && typeof value === "object" && k in value) {
114
+ value = value[k];
115
+ } else {
116
+ return undefined;
117
+ }
118
+ }
119
+
120
+ return value;
121
+ }
122
+
123
+ // Whitelist of config keys that can be modified via CLI
124
+ const ALLOWED_CONFIG_KEYS = [
125
+ "defaults.session",
126
+ "defaults.headed",
127
+ "defaults.channel",
128
+ "defaults.timeout",
129
+ ];
130
+
131
+ // Dangerous browser arguments that should be blocked
132
+ const DANGEROUS_BROWSER_ARGS = [
133
+ "remote-debugging-port",
134
+ "disable-web-security",
135
+ "disable-site-isolation",
136
+ "disable-features=IsolateOrigins",
137
+ "disable-setuid-sandbox",
138
+ ];
139
+
140
+ export async function setConfigValue(key: string, value: unknown): Promise<void> {
141
+ // Security: Whitelist check - only allow safe config keys
142
+ if (!ALLOWED_CONFIG_KEYS.includes(key)) {
143
+ throw new Error(
144
+ `Config key '${key}' cannot be modified via CLI. ` +
145
+ `Allowed keys: ${ALLOWED_CONFIG_KEYS.join(", ")}`,
146
+ );
147
+ }
148
+
149
+ // Security: Validate browser.args if being set
150
+ if (key === "browser.args" && Array.isArray(value)) {
151
+ for (const arg of value as string[]) {
152
+ for (const dangerous of DANGEROUS_BROWSER_ARGS) {
153
+ if (arg.toLowerCase().includes(dangerous)) {
154
+ throw new Error(`Dangerous browser argument blocked: ${arg}`);
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ const config = await loadConfig();
161
+ const keys = key.split(".");
162
+
163
+ if (keys.length === 0) {
164
+ throw new Error("Invalid config key");
165
+ }
166
+
167
+ let obj: any = config;
168
+ for (let i = 0; i < keys.length - 1; i++) {
169
+ const k = keys[i];
170
+ if (!(k in obj) || typeof obj[k] !== "object") {
171
+ obj[k] = {};
172
+ }
173
+ obj = obj[k];
174
+ }
175
+
176
+ const lastKey = keys[keys.length - 1];
177
+ obj[lastKey] = value;
178
+
179
+ // Validate updated config
180
+ ConfigSchema.parse(config);
181
+
182
+ await saveConfig(config);
183
+ }
@@ -0,0 +1,121 @@
1
+ export class HBAError extends Error {
2
+ code: string;
3
+ hint?: string;
4
+
5
+ constructor(message: string, code: string, hint?: string) {
6
+ super(message);
7
+ this.name = this.constructor.name;
8
+ this.code = code;
9
+ this.hint = hint;
10
+ Error.captureStackTrace(this, this.constructor);
11
+ }
12
+ }
13
+
14
+ export class SessionNotFoundError extends HBAError {
15
+ constructor(sessionName: string) {
16
+ super(
17
+ `Session '${sessionName}' not found`,
18
+ "SESSION_NOT_FOUND",
19
+ "Run 'hab sessions' to see available sessions",
20
+ );
21
+ }
22
+ }
23
+
24
+ export class ElementNotFoundError extends HBAError {
25
+ constructor(selector: string) {
26
+ super(
27
+ `Element not found: ${selector}`,
28
+ "ELEMENT_NOT_FOUND",
29
+ "Run 'hab snapshot -i' to see available elements",
30
+ );
31
+ }
32
+ }
33
+
34
+ export class BrowserNotRunningError extends HBAError {
35
+ constructor() {
36
+ super("Browser not running", "BROWSER_NOT_RUNNING", "Run 'hab open <url>' first");
37
+ }
38
+ }
39
+
40
+ export class TimeoutError extends HBAError {
41
+ constructor(operation: string, timeout: number) {
42
+ super(
43
+ `Operation timed out: ${operation} (${timeout}ms)`,
44
+ "TIMEOUT",
45
+ "Increase timeout with --timeout option",
46
+ );
47
+ }
48
+ }
49
+
50
+ export class NavigationError extends HBAError {
51
+ constructor(url: string, reason?: string) {
52
+ super(
53
+ `Navigation failed: ${url}${reason ? ` (${reason})` : ""}`,
54
+ "NAVIGATION_FAILED",
55
+ "Check if the URL is accessible and valid",
56
+ );
57
+ }
58
+ }
59
+
60
+ export class SelectorError extends HBAError {
61
+ constructor(selector: string) {
62
+ super(
63
+ `Invalid selector: ${selector}`,
64
+ "INVALID_SELECTOR",
65
+ "Use @eN, css=..., text=..., or xpath=...",
66
+ );
67
+ }
68
+ }
69
+
70
+ export class ConfigError extends HBAError {
71
+ constructor(message: string) {
72
+ super(message, "CONFIG_ERROR", "Check ~/.hab/config.json for syntax errors");
73
+ }
74
+ }
75
+
76
+ export class PermissionError extends HBAError {
77
+ constructor(path: string) {
78
+ super(`Permission denied: ${path}`, "PERMISSION_ERROR", "Check file/directory permissions");
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Format error for CLI output
84
+ */
85
+ export function formatError(error: Error): string {
86
+ let output = `Error: ${error.message}`;
87
+
88
+ if (error instanceof HBAError) {
89
+ if (error.hint) {
90
+ output += `\n Hint: ${error.hint}`;
91
+ }
92
+ output += `\n Code: ${error.code}`;
93
+ }
94
+
95
+ return output;
96
+ }
97
+
98
+ /**
99
+ * Get exit code for error type
100
+ */
101
+ export function getExitCode(error: Error): number {
102
+ if (error instanceof HBAError) {
103
+ switch (error.code) {
104
+ case "INVALID_SELECTOR":
105
+ return 2; // Argument error
106
+ case "SESSION_NOT_FOUND":
107
+ case "CONFIG_ERROR":
108
+ return 3; // Session error
109
+ case "BROWSER_NOT_RUNNING":
110
+ case "NAVIGATION_FAILED":
111
+ return 4; // Browser error
112
+ case "ELEMENT_NOT_FOUND":
113
+ return 5; // Element error
114
+ case "TIMEOUT":
115
+ return 6; // Timeout
116
+ default:
117
+ return 1; // General error
118
+ }
119
+ }
120
+ return 1; // General error
121
+ }
@@ -0,0 +1,12 @@
1
+ export function log(message: string, verbose = false) {
2
+ if (verbose) {
3
+ console.log(`[HBA] ${message}`);
4
+ }
5
+ }
6
+
7
+ export function error(message: string, hint?: string) {
8
+ console.error(`Error: ${message}`);
9
+ if (hint) {
10
+ console.error(` Hint: ${hint}`);
11
+ }
12
+ }
@@ -0,0 +1,23 @@
1
+ export type SelectorType = "ref" | "css" | "text" | "xpath";
2
+
3
+ export interface ParsedSelector {
4
+ type: SelectorType;
5
+ value: string;
6
+ }
7
+
8
+ export function parseSelector(selector: string): ParsedSelector {
9
+ if (selector.startsWith("@e")) {
10
+ return { type: "ref", value: selector.slice(1) };
11
+ }
12
+ if (selector.startsWith("css=")) {
13
+ return { type: "css", value: selector.slice(4) };
14
+ }
15
+ if (selector.startsWith("text=")) {
16
+ return { type: "text", value: selector.slice(5) };
17
+ }
18
+ if (selector.startsWith("xpath=")) {
19
+ return { type: "xpath", value: selector.slice(6) };
20
+ }
21
+ // Default to CSS
22
+ return { type: "css", value: selector };
23
+ }