haltija 1.13.0-beta.5 → 1.13.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/CHANGELOG.md CHANGED
@@ -1,5 +1,84 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.0 (2026-10-18)
4
+
5
+ The 1.13 betas, released. Read **Security** and **Upgrading from 1.12** before moving off 1.12.x.
6
+ The beta sections below have the detail and the reasoning; this section is the list. The changes
7
+ made after beta.5 are at the end.
8
+
9
+ ### Security
10
+
11
+ - **Through 1.12.9, anything on your network could drive your browser.** The server listens on
12
+ every interface and, without `--token`, answered any machine, `/eval` included. Affected: every
13
+ release through **1.12.9** (the previous `latest`) and **1.13.0-beta.1 to beta.4**. In 1.13.0 a
14
+ connection from another machine is refused (403) unless the server has a token (virta #815,
15
+ GitHub #44; first shipped in beta.5). If you stay on 1.12.x, start the server with `--token
16
+ <secret>` on any network you don't trust.
17
+ - **Pages on this machine can still call the API, and that is by design.** beta.1 listed this as a
18
+ known issue with "an origin gate planned before 1.13.0". That gate is not coming: the widget is
19
+ injected into arbitrary pages, so the REST surface must answer any origin, and an origin check
20
+ would break exactly the pages haltija exists for. `--token` closes it, as it closes a tunnel or
21
+ reverse proxy running on this machine (which connects from localhost). The README's Security
22
+ section says what is and isn't covered.
23
+
24
+ ### Upgrading from 1.12
25
+
26
+ Each of these changes something that worked before. Before → after:
27
+
28
+ - **Other machines need a token.** Phones, headsets, other computers, and browsers in a container,
29
+ VM or WSL2 that reach haltija through a forwarded port: they connected freely, and now get a 403
30
+ until the server has `--token <secret>` and the client sends it (beta.5).
31
+ - **Both transports by default.** `DEV_CHANNEL_MODE` was `http`; it is `both`, so HTTPS 8701 comes
32
+ up too. `--http` opts out, and says what that costs other projects (beta.1).
33
+ - **The certificate lives in `~/.haltija/certs`.** It was per-install; an existing one is copied
34
+ forward, so trust you already granted survives (beta.1).
35
+ - **The desktop app's internal server moved from 8701 to 8710.** `HALTIJA_PORT=8701 hj tree`
36
+ (inspecting the app's own UI) is now `HALTIJA_PORT=8710 hj tree` (beta.1).
37
+ - **Only the server on 8700 takes HTTPS 8701.** A `--port` / `--name` server's HTTPS is now
38
+ ephemeral, shown in its banner and `/status`, unless you pass `--https-port` (beta.1).
39
+ - **`server` from `haltija/server` is the HTTP listener**, and `server`, `httpServer` and
40
+ `httpsServer` are typed `ListeningServer` (`port`, `hostname`, `url`, `stop()`) instead of
41
+ Bun's server type. Code that used other Bun server methods on them needs a cast (beta.1).
42
+ - **Navigate and refresh wait for the page's widget.** They returned at once; now they block until
43
+ the widget reconnects, up to `--timeout` (default 10 s). A page that doesn't load the widget
44
+ itself (a bookmarklet tab) waits that long every time: pass `--no-wait` there. `navigate()` and
45
+ `refresh()` in `DevChannelClient` / `haltija/test` now return `{ reconnected, warning? }` (beta.2).
46
+ - **The widget writes `window.name` while navigating**, and restores the page's own value after.
47
+ An app that keeps state in `window.name` can see the marker briefly (beta.2).
48
+ - **`press` acts at the focused element.** In `haltija/test` and `DevChannelClient.press` it was one
49
+ `keydown` at `body`; now it is the full key sequence at the focused element, like `hj key`
50
+ (beta.4).
51
+ - **Schematics are drawn by tosijs-floorplan.** Same response fields; the picture looks different
52
+ (beta.5).
53
+
54
+ ### Fixed after beta.5
55
+
56
+ From the 1.13.0 release review (`reviews/1.13.0-*.md`):
57
+
58
+ - **A refused connection from another machine is now logged on the server**, once per address, with
59
+ the `--token` remedy. A browser cannot read why a WebSocket handshake failed, so a widget in a
60
+ container or VM retried forever while the server said nothing.
61
+ - **`DevChannelClient` and `haltija/test` send `HALTIJA_TOKEN`.** `hj` and the MCP bridge always
62
+ did; these two never did, so a test suite could not reach a `--token` server at all, which since
63
+ beta.5 is the setup for any runner on another machine.
64
+ - **`haltija --token=<secret>` sets a token.** The launcher read only `--token <secret>`, so the `=`
65
+ form, like a misspelled flag, started a server with no token and no complaint. `--flag=value` now
66
+ works for every launcher flag that takes a value, an unknown flag is reported, and the banner has
67
+ an `AUTH:` line saying whether a token is in force.
68
+ - **`/drag`, `hj drag` and the `drag` step move pointer-driven controls.** They sent bare mouse
69
+ events (some of them `CustomEvent`s with no coordinates), so a slider, resize handle or orbit
70
+ camera that listens for pointer events didn't move while the drag reported success. They now use
71
+ the same pointer-and-mouse sequence as `dragFrom`, with `buttons` set while pressed.
72
+ - **MCP setup replaces a stale entry.** Before 1.13 setup wrote `node …/apps/mcp/build/index.js`,
73
+ which no longer exists. `--check` reported such an entry as configured, and re-running setup said
74
+ "already configured". Both now call it stale, and setup rewrites it. The help named
75
+ `npx haltija-mcp-setup`, a package that does not exist; it is `npx -p haltija haltija-mcp-setup`.
76
+ - **The `test:integration` lane runs its tests.** It needed a server it was never given, so every
77
+ 1.13 beta attested "0 pass, 11 skip" as a pass. It now starts its own private headless instance,
78
+ and a lane that only skipped fails.
79
+ - Docs: the README no longer shows a demo image that doesn't exist, and links the API reference and
80
+ the browser-test guide (`docs/TEST-IN-BROWSER.md`).
81
+
3
82
  ## 1.13.0-beta.5 (2026-10-18)
4
83
 
5
84
  ### Changed
package/README.md CHANGED
@@ -8,9 +8,6 @@
8
8
 
9
9
  Unlike screenshot-based tools, Haltija works with the actual DOM. Unlike Playwright, it connects to your real browser — already logged in, same cookies, same bug you're looking at.
10
10
 
11
- <!-- TODO: Replace with actual GIF showing: 1) bunx haltija launches app, 2) agent queries tree, 3) agent clicks something, 4) highlight shows result -->
12
- ![Haltija in action](docs/assets/demo.gif)
13
-
14
11
  > *Haltija* (Finnish): a guardian spirit that watches over a place. In this case, your DOM.
15
12
 
16
13
  ---
@@ -337,7 +334,9 @@ app also exists but isn't required for either.
337
334
  8701 by default), but without a token it refuses every connection that doesn't come from this
338
335
  machine. To use haltija from another device (a phone, a headset, a second computer), start it
339
336
  with `--token <secret>` (or `HALTIJA_TOKEN`) and send the same value as `X-Haltija-Token`,
340
- `?token=`, or the widget's `token` option.
337
+ `?token=`, or the widget's `token` option. A browser in a container, VM or WSL2 that reaches
338
+ haltija through a forwarded port also counts as another machine. The server's terminal names
339
+ each refused address once.
341
340
  - **On this machine, any page you visit can call it.** The widget is injected into arbitrary
342
341
  pages, so the REST API answers any origin, `/eval` in your connected tabs included. That is by
343
342
  design for a local dev tool, and `--token` closes it too. A tunnel or reverse proxy on this
@@ -411,10 +410,11 @@ hj api # Full API reference (markdown)
411
410
  hj --help # CLI subcommand reference
412
411
  ```
413
412
 
414
- - [Full API Reference](./API.md) (auto-generated from schema)
413
+ - [Full API Reference](https://github.com/tonioloewald/haltija/blob/main/API.md) (auto-generated from schema)
415
414
  - [Agent Prompt](docs/agent-prompt.md) — Copy-paste prompt for any AI agent
416
415
  - [Recipes](docs/recipes.md) — Common workflows
417
416
  - [CI Integration](docs/CI-INTEGRATION.md) — E2E testing with Haltija in GitHub Actions
417
+ - [Browser tests from your test runner](docs/TEST-IN-BROWSER.md) — `createBrowserPage` from `haltija/test`: assertions on the host, probes in a real browser
418
418
  - [Roadmap](docs/ROADMAP.md)
419
419
 
420
420
  ---
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haltija-desktop",
3
- "version": "1.13.0-beta.5",
3
+ "version": "1.13.0",
4
4
  "private": true,
5
5
  "description": "Haltija Desktop - God Mode Browser for AI Agents",
6
6
  "homepage": "https://github.com/tonioloewald/haltija",
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.13.0-beta.5";
49
+ var VERSION = "1.13.0";
50
50
 
51
51
  // src/ws-url.ts
52
52
  function httpBaseFromWsUrl(wsUrl2, fallback = "http://localhost:8700") {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The launcher's own flags, and `--flag=value` support for the ones that take a value.
3
+ *
4
+ * The launcher read flags with `args.indexOf('--token')`, so `--token=secret` and a typo like
5
+ * `--tokn secret` both started a server with NO token, silently (1.13.0 review). With the #815 gate
6
+ * that mostly fails closed for other machines, but a tunnel on this machine connects from loopback,
7
+ * and the README sends tunnel users to `--token`. So: accept the `=` form, and say when a flag is not
8
+ * one the launcher knows.
9
+ */
10
+ export const VALUE_FLAGS = [
11
+ '--port', '--https-port', '--token', '--name', '--port-file',
12
+ '--headless-url', '--snapshots-dir', '--docs-dir',
13
+ ]
14
+ export const BOOLEAN_FLAGS = [
15
+ '--app', '--server', '--headless', '--ci', '--http', '--https', '--both', '--force', '-f',
16
+ '--wait-ready', '--private', '--setup-mcp', '--setup-mcp-check', '--setup-mcp-remove',
17
+ '--help', '-h',
18
+ ]
19
+
20
+ /** `--token=x` becomes `--token x`, for the value flags only. Returns the unknown flags it saw. */
21
+ export function normalizeLauncherArgs(args) {
22
+ const out = []
23
+ const unknown = []
24
+ for (const a of args) {
25
+ const eq = a.indexOf('=')
26
+ const flag = a.startsWith('-') && eq > 0 ? a.slice(0, eq) : a
27
+ if (flag !== a && VALUE_FLAGS.includes(flag)) {
28
+ out.push(flag, a.slice(eq + 1))
29
+ continue
30
+ }
31
+ if (a.startsWith('-') && !VALUE_FLAGS.includes(a) && !BOOLEAN_FLAGS.includes(a)) unknown.push(a)
32
+ out.push(a)
33
+ }
34
+ return { args: out, unknown }
35
+ }
@@ -31,3 +31,20 @@ export function mcpServerCommand(serverPath, root = PACKAGE_ROOT) {
31
31
  }
32
32
  return { command: 'node', args: [serverPath] }
33
33
  }
34
+
35
+ /**
36
+ * Why an existing `haltija` MCP entry can no longer start, or null when it looks runnable.
37
+ *
38
+ * Before 1.13.0 setup wrote `node …/apps/mcp/build/index.js`, a path the package no longer has, and
39
+ * `--check` reported such an entry as configured while re-running setup said "already configured",
40
+ * so nothing short of hand-editing the config could fix it. An entry that names a server file which
41
+ * is not there is stale, whatever else it says; one that runs a command (`npx …`) is left alone.
42
+ */
43
+ export function staleMcpEntry(entry) {
44
+ const args = Array.isArray(entry?.args) ? entry.args : []
45
+ const script = args.find((a) => typeof a === 'string' && /\.(m?js|ts)$/.test(a))
46
+ if (!script) return null
47
+ if (/[\\/]apps[\\/]mcp[\\/]build[\\/]/.test(script)) return `it runs ${script}, the pre-1.13 server location`
48
+ if (!existsSync(script)) return `it runs ${script}, which does not exist`
49
+ return null
50
+ }
package/bin/mcp-setup.mjs CHANGED
@@ -3,18 +3,18 @@
3
3
  * haltija-mcp-setup - Configure Claude Code to use Haltija
4
4
  *
5
5
  * Usage:
6
- * npx haltija-mcp-setup # Create .mcp.json in current directory
7
- * npx haltija-mcp-setup --global # Add to user's Claude Code config
8
- * npx haltija-mcp-setup --check # Check if Haltija MCP is configured
9
- * npx haltija-mcp-setup --help # Show help
6
+ * npx -p haltija haltija-mcp-setup # Create .mcp.json in current directory
7
+ * npx -p haltija haltija-mcp-setup --global # Add to user's Claude Code config
8
+ * npx -p haltija haltija-mcp-setup --check # Check if Haltija MCP is configured
9
+ * npx -p haltija haltija-mcp-setup --help # Show help
10
10
  */
11
11
 
12
12
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs'
13
13
  import { homedir } from 'os'
14
14
  import { join, dirname } from 'path'
15
15
  import { fileURLToPath } from 'url'
16
- import { execSync } from 'child_process'
17
- import { mcpServerPath, mcpServerCommand } from './mcp-locate.mjs'
16
+ import { execFileSync } from 'child_process'
17
+ import { mcpServerPath, mcpServerCommand, staleMcpEntry } from './mcp-locate.mjs'
18
18
 
19
19
  const __dirname = dirname(fileURLToPath(import.meta.url))
20
20
  const args = process.argv.slice(2)
@@ -31,11 +31,11 @@ if (args.includes('--help') || args.includes('-h')) {
31
31
  ${bold('haltija-mcp-setup')} - Configure Claude Code to use Haltija
32
32
 
33
33
  ${bold('Usage:')}
34
- npx haltija-mcp-setup Create .mcp.json in current directory (recommended)
35
- npx haltija-mcp-setup --global Add to user's global Claude Code config
36
- npx haltija-mcp-setup --check Check current MCP configuration status
37
- npx haltija-mcp-setup --remove Remove Haltija from MCP configuration
38
- npx haltija-mcp-setup --help Show this help
34
+ npx -p haltija haltija-mcp-setup Create .mcp.json in current directory (recommended)
35
+ npx -p haltija haltija-mcp-setup --global Add to user's global Claude Code config
36
+ npx -p haltija haltija-mcp-setup --check Check current MCP configuration status
37
+ npx -p haltija haltija-mcp-setup --remove Remove Haltija from MCP configuration
38
+ npx -p haltija haltija-mcp-setup --help Show this help
39
39
 
40
40
  ${bold('What this does:')}
41
41
  Configures Claude Code to connect to the Haltija server, giving Claude
@@ -84,7 +84,11 @@ function checkConfig() {
84
84
  if (existsSync(projectMcpPath)) {
85
85
  try {
86
86
  const config = JSON.parse(readFileSync(projectMcpPath, 'utf8'))
87
- if (config.mcpServers?.haltija) {
87
+ const stale = config.mcpServers?.haltija && staleMcpEntry(config.mcpServers.haltija)
88
+ if (stale) {
89
+ console.log(red('✗') + ` Project .mcp.json has a stale Haltija entry: ${stale}`)
90
+ console.log(dim(' Run setup again to replace it: npx -p haltija haltija-mcp-setup'))
91
+ } else if (config.mcpServers?.haltija) {
88
92
  console.log(green('✓') + ` Project .mcp.json has Haltija configured`)
89
93
  console.log(dim(` Path: ${projectMcpPath}`))
90
94
  } else {
@@ -108,9 +112,10 @@ function checkConfig() {
108
112
  }
109
113
 
110
114
  // Check if Haltija server is running
115
+ const port = process.env.HALTIJA_PORT || '8700'
111
116
  try {
112
- execSync('curl -s http://localhost:8700/status', { encoding: 'utf8', timeout: 2000 })
113
- console.log(green('✓') + ' Haltija server is running on port 8700')
117
+ execFileSync('curl', ['-sf', `http://localhost:${port}/status`], { encoding: 'utf8', timeout: 2000 })
118
+ console.log(green('✓') + ` Haltija server is running on port ${port}`)
114
119
  } catch {
115
120
  console.log(yellow('○') + ' Haltija server not running')
116
121
  console.log(dim(' Start with: bunx haltija'))
@@ -145,7 +150,10 @@ function setupProjectConfig() {
145
150
  }
146
151
 
147
152
  // Check if already configured
148
- if (config.mcpServers.haltija) {
153
+ const stale = config.mcpServers.haltija && staleMcpEntry(config.mcpServers.haltija)
154
+ if (stale) {
155
+ console.log(yellow('Replacing a stale Haltija entry in .mcp.json:') + ` ${stale}`)
156
+ } else if (config.mcpServers.haltija) {
149
157
  console.log(yellow('Haltija is already configured in .mcp.json'))
150
158
  console.log(dim('Use --remove to remove it, or delete .mcp.json manually'))
151
159
  process.exit(0)
@@ -178,7 +186,9 @@ function setupGlobalConfig() {
178
186
  const addCommand = `claude mcp add --scope user haltija -- ${[command, ...commandArgs].map((a) => JSON.stringify(a)).join(' ')}`
179
187
  // Try using claude CLI first
180
188
  try {
181
- execSync(addCommand, {
189
+ // An argv, not a shell string: the install path is in the args, and a shell would run any
190
+ // `$()` or backtick in it.
191
+ execFileSync('claude', ['mcp', 'add', '--scope', 'user', 'haltija', '--', command, ...commandArgs], {
182
192
  encoding: 'utf8',
183
193
  stdio: 'inherit'
184
194
  })
@@ -1,6 +1,11 @@
1
1
  /** ⚠️ AUTO-GENERATED FROM src/server-list.ts — DO NOT EDIT. Run: bun run build */
2
+ // src/ports.ts
3
+ var DEFAULT_HTTP_PORT = 8700;
4
+ var DEFAULT_HTTPS_PORT = 8701;
5
+ var PUBLIC_PORTS = [DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT];
6
+
2
7
  // src/server-list.ts
3
- function collectCandidates(instances, resolvedPort, defaults = [8700, 8701]) {
8
+ function collectCandidates(instances, resolvedPort, defaults = [...PUBLIC_PORTS]) {
4
9
  const byPort = new Map;
5
10
  for (const e of instances) {
6
11
  byPort.set(String(e.port), { port: String(e.port), name: e.name, cwd: e.cwd ?? null });
@@ -18,7 +18,7 @@ import { homedir, platform, tmpdir } from 'os'
18
18
  import { fileURLToPath } from 'url'
19
19
  import { dirname, join } from 'path'
20
20
  import { stalePrivateEntries, pidIsAlive } from './private-state.mjs'
21
- import { mcpServerPath, mcpServerCommand } from './mcp-locate.mjs'
21
+ import { mcpServerPath, mcpServerCommand, staleMcpEntry } from './mcp-locate.mjs'
22
22
 
23
23
  const __dirname = dirname(fileURLToPath(import.meta.url))
24
24
  const serverPath = join(__dirname, '../dist/server.js')
@@ -171,7 +171,11 @@ function checkMcpConfig() {
171
171
  if (existsSync(configPath)) {
172
172
  try {
173
173
  const config = JSON.parse(readFileSync(configPath, 'utf8'))
174
- if (config.mcpServers?.haltija) {
174
+ const stale = config.mcpServers?.haltija && staleMcpEntry(config.mcpServers.haltija)
175
+ if (stale) {
176
+ console.log(red('✗') + ` Haltija's Claude Desktop entry is stale: ${stale}`)
177
+ console.log(dim(` Run: haltija --setup-mcp (it replaces the entry)`))
178
+ } else if (config.mcpServers?.haltija) {
175
179
  console.log(green('✓') + ' Haltija is configured in Claude Desktop')
176
180
  console.log(dim(` Command: ${config.mcpServers.haltija.command} ${config.mcpServers.haltija.args?.join(' ') || ''}`))
177
181
  } else {
@@ -248,7 +252,10 @@ function setupMcp() {
248
252
  }
249
253
 
250
254
  // Check if already configured
251
- if (config.mcpServers.haltija) {
255
+ const stale = config.mcpServers.haltija && staleMcpEntry(config.mcpServers.haltija)
256
+ if (stale) {
257
+ console.log(yellow('Replacing a stale Haltija entry:') + ` ${stale}`)
258
+ } else if (config.mcpServers.haltija) {
252
259
  console.log(green('✓') + ' Haltija is already configured in Claude Desktop')
253
260
  console.log(dim(` Config: ${configPath}`))
254
261
  console.log('')
@@ -310,6 +317,7 @@ function removeMcp() {
310
317
  // ============================================
311
318
 
312
319
  import { isSubcommand, runSubcommand } from './cli-subcommand.mjs'
320
+ import { normalizeLauncherArgs } from './launcher-args.mjs'
313
321
 
314
322
  // Check if first positional arg is a subcommand (not a flag, not a port number)
315
323
  const firstNonFlag = args.find(a => !a.startsWith('-'))
@@ -349,6 +357,16 @@ if (args.includes('--setup-mcp-remove')) {
349
357
  process.exit(0)
350
358
  }
351
359
 
360
+ // Launcher mode from here on (subcommands took their own flags above). `--flag=value` is split
361
+ // in place, so every `args.indexOf('--flag')` below sees it; an unknown flag is said out loud.
362
+ {
363
+ const { args: normalized, unknown } = normalizeLauncherArgs(args)
364
+ args.splice(0, args.length, ...normalized)
365
+ for (const flag of unknown) {
366
+ console.error(yellow(`[haltija] unknown option ${flag} is ignored (see haltija --help)`))
367
+ }
368
+ }
369
+
352
370
  // Set up environment from args
353
371
  const env = { ...process.env }
354
372
 
package/bin/version.mjs CHANGED
@@ -3,4 +3,4 @@
3
3
  * ⚠️ To change the version, update package.json and run: bun run build
4
4
  */
5
5
 
6
- export const HJ_VERSION = '1.13.0-beta.5'
6
+ export const HJ_VERSION = '1.13.0'
@@ -297,8 +297,8 @@ export declare const recordingStart: EndpointDef<{
297
297
  export declare const recordingStop: EndpointDef<{}>;
298
298
  export declare const recordingGenerate: EndpointDef<{
299
299
  url?: string | undefined;
300
- events?: any[] | undefined;
301
300
  minDelay?: number | undefined;
301
+ events?: any[] | undefined;
302
302
  name?: string | undefined;
303
303
  since?: number | undefined;
304
304
  description?: string | undefined;
@@ -654,8 +654,8 @@ export declare const endpoints: {
654
654
  readonly recordingStop: EndpointDef<{}>;
655
655
  readonly recordingGenerate: EndpointDef<{
656
656
  url?: string | undefined;
657
- events?: any[] | undefined;
658
657
  minDelay?: number | undefined;
658
+ events?: any[] | undefined;
659
659
  name?: string | undefined;
660
660
  since?: number | undefined;
661
661
  description?: string | undefined;
@@ -931,8 +931,8 @@ export declare const ALL_ENDPOINTS: (EndpointDef<{
931
931
  name?: string | undefined;
932
932
  }> | EndpointDef<{}> | EndpointDef<{
933
933
  url?: string | undefined;
934
- events?: any[] | undefined;
935
934
  minDelay?: number | undefined;
935
+ events?: any[] | undefined;
936
936
  name?: string | undefined;
937
937
  since?: number | undefined;
938
938
  description?: string | undefined;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `fetch`, plus `X-Haltija-Token` when `HALTIJA_TOKEN` is set: what `DevChannelClient` and
3
+ * `haltija/test` send every request with.
4
+ *
5
+ * `hj` and the MCP bridge have always sent the token; these two never did, so a test suite could
6
+ * not reach a server started with `--token` at all. Since 1.13.0 a caller on another machine (or in
7
+ * a container reaching the host through a forwarded port) needs that token (#815), so the gap went
8
+ * from rare to the documented setup (1.13.0 review). Imported as `fetch` so no call site changes,
9
+ * and a header the caller set explicitly wins.
10
+ */
11
+ export declare function authedFetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
package/dist/client.js CHANGED
@@ -1,4 +1,15 @@
1
1
  // @bun
2
+ // src/client-auth.ts
3
+ function authedFetch(input, init = {}) {
4
+ const token = typeof process !== "undefined" ? process.env.HALTIJA_TOKEN : undefined;
5
+ if (!token)
6
+ return fetch(input, init);
7
+ const headers = new Headers(init.headers);
8
+ if (!headers.has("X-Haltija-Token"))
9
+ headers.set("X-Haltija-Token", token);
10
+ return fetch(input, { ...init, headers });
11
+ }
12
+
2
13
  // src/client.ts
3
14
  class DevChannelClient {
4
15
  baseUrl;
@@ -6,11 +17,11 @@ class DevChannelClient {
6
17
  this.baseUrl = serverUrl;
7
18
  }
8
19
  async status() {
9
- const res = await fetch(`${this.baseUrl}/status`);
20
+ const res = await authedFetch(`${this.baseUrl}/status`);
10
21
  return res.json();
11
22
  }
12
23
  async send(channel, action, payload) {
13
- const res = await fetch(`${this.baseUrl}/send`, {
24
+ const res = await authedFetch(`${this.baseUrl}/send`, {
14
25
  method: "POST",
15
26
  headers: { "Content-Type": "application/json" },
16
27
  body: JSON.stringify({ channel, action, payload })
@@ -18,7 +29,7 @@ class DevChannelClient {
18
29
  return res.json();
19
30
  }
20
31
  async request(channel, action, payload, timeout = 5000) {
21
- const res = await fetch(`${this.baseUrl}/request`, {
32
+ const res = await authedFetch(`${this.baseUrl}/request`, {
22
33
  method: "POST",
23
34
  headers: { "Content-Type": "application/json" },
24
35
  body: JSON.stringify({ channel, action, payload, timeout })
@@ -26,7 +37,7 @@ class DevChannelClient {
26
37
  return res.json();
27
38
  }
28
39
  async query(selector) {
29
- const res = await fetch(`${this.baseUrl}/query`, {
40
+ const res = await authedFetch(`${this.baseUrl}/query`, {
30
41
  method: "POST",
31
42
  headers: { "Content-Type": "application/json" },
32
43
  body: JSON.stringify({ selector, all: false })
@@ -35,7 +46,7 @@ class DevChannelClient {
35
46
  return response.success ? response.data : null;
36
47
  }
37
48
  async queryAll(selector) {
38
- const res = await fetch(`${this.baseUrl}/query`, {
49
+ const res = await authedFetch(`${this.baseUrl}/query`, {
39
50
  method: "POST",
40
51
  headers: { "Content-Type": "application/json" },
41
52
  body: JSON.stringify({ selector, all: true })
@@ -44,7 +55,7 @@ class DevChannelClient {
44
55
  return response.success ? response.data : [];
45
56
  }
46
57
  async getConsole(since = 0) {
47
- const res = await fetch(`${this.baseUrl}/console?since=${since}`);
58
+ const res = await authedFetch(`${this.baseUrl}/console?since=${since}`);
48
59
  const response = await res.json();
49
60
  return response.success ? response.data : [];
50
61
  }
@@ -52,7 +63,7 @@ class DevChannelClient {
52
63
  await this.request("console", "clear", {});
53
64
  }
54
65
  async eval(code) {
55
- const res = await fetch(`${this.baseUrl}/eval`, {
66
+ const res = await authedFetch(`${this.baseUrl}/eval`, {
56
67
  method: "POST",
57
68
  headers: { "Content-Type": "application/json" },
58
69
  body: JSON.stringify({ code })
@@ -64,7 +75,7 @@ class DevChannelClient {
64
75
  return response.data;
65
76
  }
66
77
  async click(selector, options) {
67
- const res = await fetch(`${this.baseUrl}/click`, {
78
+ const res = await authedFetch(`${this.baseUrl}/click`, {
68
79
  method: "POST",
69
80
  headers: { "Content-Type": "application/json" },
70
81
  body: JSON.stringify({ selector, options })
@@ -75,7 +86,7 @@ class DevChannelClient {
75
86
  }
76
87
  }
77
88
  async type(selector, text) {
78
- const res = await fetch(`${this.baseUrl}/type`, {
89
+ const res = await authedFetch(`${this.baseUrl}/type`, {
79
90
  method: "POST",
80
91
  headers: { "Content-Type": "application/json" },
81
92
  body: JSON.stringify({ selector, text })
@@ -98,7 +109,7 @@ class DevChannelClient {
98
109
  await this.dispatch({ selector, event: "blur" });
99
110
  }
100
111
  async press(key, modifiers) {
101
- const res = await fetch(`${this.baseUrl}/key`, {
112
+ const res = await authedFetch(`${this.baseUrl}/key`, {
102
113
  method: "POST",
103
114
  headers: { "Content-Type": "application/json" },
104
115
  body: JSON.stringify({
@@ -126,7 +137,7 @@ class DevChannelClient {
126
137
  return outcome;
127
138
  }
128
139
  async refresh(hard = true) {
129
- const res = await fetch(`${this.baseUrl}/refresh`, {
140
+ const res = await authedFetch(`${this.baseUrl}/refresh`, {
130
141
  method: "POST",
131
142
  headers: { "Content-Type": "application/json" },
132
143
  body: JSON.stringify({ soft: !hard })
@@ -138,7 +149,7 @@ class DevChannelClient {
138
149
  return this.navigationOutcome(response);
139
150
  }
140
151
  async navigate(url) {
141
- const res = await fetch(`${this.baseUrl}/navigate`, {
152
+ const res = await authedFetch(`${this.baseUrl}/navigate`, {
142
153
  method: "POST",
143
154
  headers: { "Content-Type": "application/json" },
144
155
  body: JSON.stringify({ url })
@@ -150,7 +161,7 @@ class DevChannelClient {
150
161
  return this.navigationOutcome(response);
151
162
  }
152
163
  async getLocation() {
153
- const res = await fetch(`${this.baseUrl}/location`);
164
+ const res = await authedFetch(`${this.baseUrl}/location`);
154
165
  const response = await res.json();
155
166
  if (!response.success) {
156
167
  throw new Error(response.error || "Get location failed");
@@ -158,7 +169,7 @@ class DevChannelClient {
158
169
  return response.data;
159
170
  }
160
171
  async startRecording(name) {
161
- const res = await fetch(`${this.baseUrl}/recording/start`, {
172
+ const res = await authedFetch(`${this.baseUrl}/recording/start`, {
162
173
  method: "POST",
163
174
  headers: { "Content-Type": "application/json" },
164
175
  body: JSON.stringify({ name })
@@ -170,7 +181,7 @@ class DevChannelClient {
170
181
  return response.data.sessionId;
171
182
  }
172
183
  async stopRecording() {
173
- const res = await fetch(`${this.baseUrl}/recording/stop`, {
184
+ const res = await authedFetch(`${this.baseUrl}/recording/stop`, {
174
185
  method: "POST"
175
186
  });
176
187
  const response = await res.json();
@@ -186,7 +197,7 @@ class DevChannelClient {
186
197
  }
187
198
  }
188
199
  async publishBuild(event) {
189
- await fetch(`${this.baseUrl}/build`, {
200
+ await authedFetch(`${this.baseUrl}/build`, {
190
201
  method: "POST",
191
202
  headers: { "Content-Type": "application/json" },
192
203
  body: JSON.stringify(event)
@@ -206,7 +217,7 @@ class DevChannelClient {
206
217
  }
207
218
  }
208
219
  async watchMutations(options) {
209
- const res = await fetch(`${this.baseUrl}/mutations/watch`, {
220
+ const res = await authedFetch(`${this.baseUrl}/mutations/watch`, {
210
221
  method: "POST",
211
222
  headers: { "Content-Type": "application/json" },
212
223
  body: JSON.stringify(options || {})
@@ -217,7 +228,7 @@ class DevChannelClient {
217
228
  }
218
229
  }
219
230
  async unwatchMutations() {
220
- const res = await fetch(`${this.baseUrl}/mutations/unwatch`, {
231
+ const res = await authedFetch(`${this.baseUrl}/mutations/unwatch`, {
221
232
  method: "POST"
222
233
  });
223
234
  const response = await res.json();
@@ -226,7 +237,7 @@ class DevChannelClient {
226
237
  }
227
238
  }
228
239
  async getMutationStatus() {
229
- const res = await fetch(`${this.baseUrl}/mutations/status`);
240
+ const res = await authedFetch(`${this.baseUrl}/mutations/status`);
230
241
  const response = await res.json();
231
242
  if (!response.success) {
232
243
  throw new Error(response.error || "Get mutation status failed");
@@ -234,7 +245,7 @@ class DevChannelClient {
234
245
  return response.data;
235
246
  }
236
247
  async getMessages(since = 0) {
237
- const res = await fetch(`${this.baseUrl}/messages?since=${since}`);
248
+ const res = await authedFetch(`${this.baseUrl}/messages?since=${since}`);
238
249
  return res.json();
239
250
  }
240
251
  async getInteractiveElements() {
@@ -306,7 +317,7 @@ class DevChannelClient {
306
317
  };
307
318
  }
308
319
  async validateTest(test) {
309
- const res = await fetch(`${this.baseUrl}/test/validate`, {
320
+ const res = await authedFetch(`${this.baseUrl}/test/validate`, {
310
321
  method: "POST",
311
322
  headers: { "Content-Type": "application/json" },
312
323
  body: JSON.stringify(test)
@@ -314,7 +325,7 @@ class DevChannelClient {
314
325
  return res.json();
315
326
  }
316
327
  async runTest(test, options) {
317
- const res = await fetch(`${this.baseUrl}/test/run`, {
328
+ const res = await authedFetch(`${this.baseUrl}/test/run`, {
318
329
  method: "POST",
319
330
  headers: { "Content-Type": "application/json" },
320
331
  body: JSON.stringify({ test, ...options })
@@ -330,7 +341,7 @@ class DevChannelClient {
330
341
  };
331
342
  }
332
343
  async runTestSuite(tests, options) {
333
- const res = await fetch(`${this.baseUrl}/test/suite`, {
344
+ const res = await authedFetch(`${this.baseUrl}/test/suite`, {
334
345
  method: "POST",
335
346
  headers: { "Content-Type": "application/json" },
336
347
  body: JSON.stringify({ tests, ...options })
@@ -339,7 +350,7 @@ class DevChannelClient {
339
350
  }
340
351
  async loadTest(source) {
341
352
  if (source.startsWith("http://") || source.startsWith("https://")) {
342
- const res = await fetch(source);
353
+ const res = await authedFetch(source);
343
354
  return res.json();
344
355
  }
345
356
  return JSON.parse(source);
@@ -20,7 +20,7 @@
20
20
  * - Option+Tab toggles visibility (but active state always shows briefly)
21
21
  * - Localhost only by default
22
22
  */
23
- export declare const VERSION = "1.13.0-beta.5";
23
+ export declare const VERSION = "1.13.0";
24
24
  export declare class DevChannel extends HTMLElement {
25
25
  static get tagName(): string;
26
26
  static elementCreator(): () => DevChannel;
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "1.13.0-beta.5";
2
+ var VERSION = "1.13.0";
3
3
 
4
4
  // src/ws-url.ts
5
5
  function httpBaseFromWsUrl(wsUrl2, fallback = "http://localhost:8700") {
package/dist/component.js CHANGED
@@ -46,7 +46,7 @@
46
46
  });
47
47
 
48
48
  // src/version.ts
49
- var VERSION = "1.13.0-beta.5";
49
+ var VERSION = "1.13.0";
50
50
 
51
51
  // src/ws-url.ts
52
52
  function httpBaseFromWsUrl(wsUrl2, fallback = "http://localhost:8700") {