Commit Graph
3 Commits
Author SHA1 Message Date
Julian Dominguez-SchatzandGitHub 42e9d79d4a Merge commit from fork
* [AI] Reject invalid password inputs before hashing

- Require non-empty strings at password validation, hashing and verification
  boundaries so JSON objects, arrays and other scalar types cannot reach
  Argon2's buffer conversion.
- Add regressions for malformed passwords on fresh and configured servers,
  including assertions that rejected input never reaches Argon2.

Advisory:
https://github.com/actualbudget/actual/security/advisories/GHSA-3jjw-2hq7-mmm2

* [AI] Require admin permission for EnableBanking configuration

- Require administrator permission to configure EnableBanking, even when
  credentials have not been configured yet.
- Check permission before validating credentials with the provider or
  persisting secrets, and disable configuration controls for non-admins.
- Test unauthenticated, BASIC and ADMIN requests against empty and existing
  configuration, preserving secrets and avoiding provider calls on denial.

Advisories:
https://github.com/actualbudget/actual/security/advisories/GHSA-hfxx-qc34-gqcw

* [AI] Neutralize BOM-prefixed CSV formulas

- Detect spreadsheet formula triggers after leading BOM characters and
  prepend an apostrophe before the original text.
- Protect textual CSV headers and scalar values as well as ordinary strings.
- Preserve numeric output, including negative amounts, and cover repeated
  BOMs, formula triggers, quoting and Unicode text in regression tests.

Advisories:
https://github.com/actualbudget/actual/security/advisories/GHSA-7jvc-qfpw-85g2

* [AI] Remove ambiguous Commerzbank payee matching

- Split Commerzbank payees into non-empty whitespace/slash-delimited tokens
  and escape their literal text before constructing the cleanup expression.
- Use consuming separators that cannot overlap the literal tokens, avoiding
  ambiguous matching on long runs of spaces or slashes.
- Require separators between payee tokens: concatenated words in notes are
  no longer stripped. Allow 20 seconds for the hostile-case subprocess,
  including Node and Vite startup on slower CI runners.
- Skip matching for empty payees while preserving normal note cleanup.
- Exercise hostile payees, including the report's spaced-word reproducer,
  in a subprocess with a hard timeout; retain the existing bank fixtures.

Advisories:
https://github.com/actualbudget/actual/security/advisories/GHSA-p34v-m2gp-w3jm

* [AI] Reject unsafe IPv6 address classes in SSRF checks

- Allow public unicast addresses by default. The private-network option
  additionally permits private, loopback, unique-local and carrier-grade NAT addresses.
- Permit the well-known NAT64 prefix only for embedded public IPv4
  destinations; private and metadata addresses remain blocked in both modes.
- Reject other IPv6 transition, multicast, reserved and link-local classes in
  both modes, and classify IPv4-mapped addresses by the embedded IPv4 address.
- Cover literal URLs, DNS answers, redirects and both SimpleFIN fetch paths,
  including NAT64, 6to4 and Teredo addresses, with synthetic upstreams.

Advisories:
https://github.com/actualbudget/actual/security/advisories/GHSA-97j5-6g5r-42v5

* [AI] Exclude callback secrets from ordinary server logs

- Log request method, pathname and response status without query strings,
  and restrict serialized metadata to the same safe fields.
- Keep full OpenID and banking provider messages, response bodies, session
  identifiers and error stacks behind existing actual-sensitive namespaces.
- Apply the same opt-in handling to shared banking errors, proxy target
  URLs and unhandled rejections; do not forward raw streaming errors to
  Express's ordinary error logger.
- Stop logging stored secret values, including with debugging enabled.
- Document targeted sensitive debugging and private support exchanges.
- Test ordinary and opt-in output, query and serialized metadata leaks,
  provider failures, session URLs, streaming errors and secret storage.

- Preserve provider format sequences by passing SimpleFIN response text as
  a debug format argument, rather than part of the format string.

Advisories:
https://github.com/actualbudget/actual/security/advisories/GHSA-ppcp-hx2h-fm35

* [AI] Revoke password sessions when credentials change

- Update the password hash and revoke all password-authenticated sessions
  in one transaction, including password changes through the local CLI.
- Issue a fresh token for each successful password login instead of reusing
  an existing session token.
- Recheck the verified password hash before creating a session so an
  in-flight login cannot restore access after a credential change.
- Correct the UI's claim that other sessions remain signed in, and test
  multiple sessions, CLI resets, transaction rollback and login/reset races.

This addresses session revocation and token reuse. Requiring the current
password for remote changes is covered by the following commit.

Advisories:
https://github.com/actualbudget/actual/security/advisories/GHSA-7363-3qq5-2frj
https://github.com/actualbudget/actual/security/advisories/GHSA-4wgp-48c6-v9w3

* [AI] Require the current password for remote password changes

- Require currentPassword alongside the replacement password for remote
  changes, preserving the administrator and password-authentication checks.
- Verify the current credential, rate-limit attempts and reject a change
  if the verified password hash changes before the update.
- Collect the current password in the client and handle failure to sign in
  after a successful change. Keep the local reset command available without
  the old password.
- Test that a session token alone cannot change credentials and that failed
  requests preserve the existing password and sessions.

- Clear the redundant success notice when signing in with the new password fails.

Advisories:
https://github.com/actualbudget/actual/security/advisories/GHSA-4wgp-48c6-v9w3

* [AI] Restrict Electron protocol launches and network file reveals

- Allow only parsed HTTP/HTTPS URLs through external-opening IPC,
  new-window handling and navigation, addressing GHSA-7pf2-w24j-p6h2.
- Also reject network/UNC paths in the separate file-reveal handler.
  A note such as [View receipt](//host/share/invoice.pdf) reaches
  showItemInFolder without passing through openExternal. On Windows,
  resolving that share can disclose NTLM authentication responses to an
  attacker-controlled server where network and authentication policies
  permit it. This additional flow was not demonstrated in the advisory.
- Accept native absolute local paths without adding file-URL conversion.
- Test protocol filtering and network-path rejection without invoking OS
  handlers. The note-to-file-reveal flow is code-confirmed; an actual
  Windows authentication exchange has not been reproduced.

The following commit addresses the advisory's separate internal-origin
classification finding.

Advisory:
https://github.com/actualbudget/actual/security/advisories/GHSA-7pf2-w24j-p6h2

Additional finding references:
https://github.com/electron/electron/blob/v43.4.0/shell/common/platform_util_win.cc#L270-L340
https://learn.microsoft.com/en-us/windows-server/storage/file-server/smb-ntlm-blocking

- Exercise Windows path semantics on all test platforms and exclude generated
  Electron tests and Vitest configuration from the packaged application.

* [AI] Classify Electron app URLs exactly and block redirects

- Replace substring checks with exact app://actual scheme/host matching
  and the existing development origin, http://localhost:3001.
- Use the classification for browser handoffs and focus/log script
  execution so remote URLs containing app:// or localhost: are not
  mistaken for app pages.
- Preserve the existing global renderer-navigation block and add redirect
  blocking there, without duplicating it in the main-window handlers.
- Keep ordinary IPC registration unchanged; broad sender validation is
  outside this URL-classification fix.
- Test misleading hosts, ports and URL substrings. Exercise ordinary
  clicks, new-window links and explicit browser IPC in Electron, asserting
  the exact HTTP/HTTPS URLs handed to the browser without opening real
  OS handlers. Exercise a local HTTP redirect to check the app stays put.

Advisory:
https://github.com/actualbudget/actual/security/advisories/GHSA-7pf2-w24j-p6h2

- Retain startup logs until an internal page finishes loading, then forward
  them to DevTools; cover startup delivery in the Electron navigation suite.

* [AI] Choose import destinations from trusted local context

- Allocate fresh local IDs for cloud downloads and local archive imports,
  ignoring destination IDs supplied in archived metadata.
- Pass an explicit local destination for replacement downloads and verify
  its cloud-file association. Do not require matching sync group IDs, so
  downloading the reset version of the same cloud file remains possible.
- Reject server metadata describing a different cloud file and close the
  budget before an authorized replacement.
- Retain existing import metadata compatibility, collision error handling,
  nested archive layouts and backups; do not require a budget name.
- Test targeted collisions, local recovery imports with obsolete IDs or
  missing optional metadata, mismatched cloud associations and replacement
  after a sync ID reset using real local files and databases.

Advisory:
https://github.com/actualbudget/actual/security/advisories/GHSA-7fr5-fjj8-hwjq

- Reuse the matching local budget for API downloads after a sync reset,
  report cloud-file mismatches in API/CLI errors, and document local IDs.

* [AI] Revoke sessions on sign-out and API shutdown

Revoke only the supplied session on explicit sign-out. Clear local credentials if the server cannot be reached, and revoke password-created API sessions during shutdown without revoking caller-supplied tokens.

Cover isolated and repeated revocation, invalid tokens, offline sign-out, borrowed API tokens and shutdown after a close failure. Session expiry policy is unchanged.

Follow-up to:
https://github.com/actualbudget/actual/security/advisories/GHSA-7363-3qq5-2frj
2026-10-08 08:34:05 -04:00
8ad218b7d9 [AI] api: make the browser build work in consumer production bundles (#8289)
* api: make the browser build work in consumer production bundles

The browser build shipped `new Worker(new URL("./worker.js", import.meta.url))`
in dist/browser.js. A consumer bundler (Vite/Rollup) recognizes that as a worker
entry and re-bundles the already-prebuilt worker.js from scratch, which desyncs
the absurd-sql/connection RPC so `init` throws a structured-clone error. It only
worked in dev and in the package's own e2e because both serve dist verbatim.

Make the browser build fully self-contained instead:

- Inline the worker into browser.js and spawn it from a Blob URL, so consumer
  bundlers never see a worker entry to re-bundle. The worker is built as an IIFE
  (classic worker), mirroring the web app's kcab.worker.
- Embed the sql.js wasm and the default filesystem data (migrations + default
  DB) into the worker so it performs no PUBLIC_URL asset fetches:
  - loot-core sqlite gains an opt-in `setWasmBinary` (dormant for web/node);
  - the worker installs a scoped fetch shim serving the embedded data/* files
    from a sentinel base URL.
- Share the asset-collection logic between the Node disk copy and the embedded
  build via scripts/embedded-assets.mjs so the two never drift.

Result: `import '@actual-app/api'` + `init()` works in any bundler with zero
config; only COOP/COEP headers remain required (SharedArrayBuffer).

Adds an e2e (e2e/consumer + e2e/global-setup.mjs) that builds a real consumer
app for production and boots it under COOP/COEP — coverage the verbatim-dist
harness can't provide. Docs note the build is self-contained and that
cross-origin isolation is still required.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] api: address review feedback (guard DOM lookup, revoke blob URL on worker failure)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] api: address review nitpicks (drop BodyInit cast, reword release note)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] api: build the browser worker via Vite ?worker&inline

Replace the hand-rolled worker inlining (a custom Vite plugin that read the
prebuilt worker.js off disk and inlined it as a string, plus a second build
config and a build-ordering dependency) with Vite's native `?worker&inline`
import. This collapses the two browser build configs into one, removes the
`virtual:actual-worker-code` module and the manual Blob URL spawn, and drops
the worker-before-facade build step.

Vite's `?worker&inline` sub-build doesn't receive vite-plugin-node-polyfills'
global-shim injection (the plugin writes it to `build.rollupOptions`, but the
worker sub-build reads `worker.rollupOptions`), so the worker crashed on
`process is not defined`. Patch the plugin to also emit the injection on
`worker.rollupOptions` — additive and backwards compatible (the web app build,
including loot-core's nodePolyfills worker, is unaffected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] api: embed browser assets via Vite, drop the embedded-assets script

Replace scripts/embedded-assets.mjs with Vite-native asset imports in the
worker entry: `?inline` for the sql.js wasm and default DB, `import.meta.glob`
(`?raw`) for the migrations. The worker builds the default-filesystem wire
format from those at module load, so the embedded set comes straight from
loot-core and can't drift.

The Node build only needs migrations + the default DB on disk (it reads them at
runtime); the old script also wrote sql-wasm.wasm and the data/ fetch tree,
which are dead now that the browser worker is self-contained. Replace
writeEmbeddedAssetsToDist with a small closeBundle copy of just those two.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] api: simplify embedded data map; drop loot-core comment churn

Merge the worker's binData/textData into a single dataFiles map (Response
accepts both bytes and strings), collapsing the fetch shim's two lookups into
one. Revert the unrelated comment edits to loot-core's backend-worker.ts and
fs/index.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] api: rename e2e consumer-dist to consumer/dist; drop a prose comment

Use the standard `dist` directory name for the consumer fixture's build output
(now e2e/consumer/dist), and remove the verbose comment from index.browser.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] loot-core: own the default-filesystem assets; consumers stop reaching in

Add packages/loot-core/default-filesystem.mjs as the single source of truth for
loot-core's runtime assets (sql.js wasm, default DB, migrations) and the
data-file-index wire format, exported as @actual-app/core/default-filesystem.

- @actual-app/api: the browser worker embeds them via the actual-embedded-assets
  Vite plugin (which calls collectEmbeddedAssets), replacing the relative
  ?inline/?raw/import.meta.glob reaches into ../loot-core; the Node build copies
  migrations + default DB using the helper's paths. Drops the direct
  @jlongster/sql.js devDependency (loot-core resolves the wasm).
- @actual-app/web: stagePublicData copies from the helper's paths instead of
  hardcoding loot-core's tree and the sql.js wasm location.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] loot-core: scan migrations once when collecting embedded assets

collectEmbeddedAssets() read the migrations directory twice — once in its
own loop and once via buildDataFileIndex(). List the names once and pass
them through, keeping the index wire-format defined in a single place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:36:21 +00:00
8f27e177dc [AI] api: add browser build (#8166)
* [AI] api: add browser build

Continuation of #7545 (and #8143, which moved the browser Web Worker
bootstrap into loot-core). This adds the browser-compatible build of
@actual-app/api: the full loot-core stack runs inside a Web Worker and a
thin main-thread facade exposes the same public surface as the Node entry.

loot-core:
- Add platform/client/backend-worker.ts: createBackendWorker(worker)
  centralizes absurd-sql's main-thread initBackend + __absurd:* message
  filtering, and is exported for the api to reuse.
- Refactor browser-preload's start.ts and worker-bridge.ts to route through
  it (drops the duplicated __absurd:* filtering; terminate() now also clears
  listeners). Behavior is preserved.

api:
- browser-worker.ts: Web Worker entry owning real loot-core, speaking
  loot-core's {id,name,args} backend protocol; wraps fetch to map the
  .js -> .js.data migration names.
- index.browser.ts: main-thread facade that spawns the worker.
- browser/rpc.ts: main-thread <-> worker RPC bridge (uses createBackendWorker).
- browser/lib-stub.ts: stubs lib.send so methods.ts routes over postMessage.
- vite.browser.config.mts / vite.browser-worker.config.mts and asset bundling
  in vite.config.mts (sql-wasm.wasm + data/ + data-file-index.txt); package.json
  build chain, browser export, and absurd-sql/npm-run-all/vite-plugin-node-polyfills deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] Rename release note to match PR number 8166

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] api: move browser worker plumbing into loot-core

Address review feedback: the browser worker logic shouldn't live in the api
package since it's generic "loot-core as a browser npm package" plumbing, not
api-specific. Resolve it via loot-core's existing conditional-export system
instead of a vite alias.

- loot-core: add server/main.api-browser.ts (worker-routing `lib` that
  delegates to the existing platform/client/connection), selected via a new
  `api-browser` export condition on "./server/main". Move the worker entry to
  server/api-browser-worker.ts.
- api: the facade (index.browser.ts) now reuses loot-core's client connection
  via a minimal global.Actual.getServerSocket shim and spawns the worker built
  from loot-core's entry. Deletes browser-worker.ts, browser/rpc.ts and
  browser/lib-stub.ts (~270 lines) in favor of the shared loot-core code.
- The facade build selects the impl with resolve.conditions: ['api-browser']
  instead of resolve.alias, removing the enforce-boundaries lint exception.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] api: simplify browser build cleanups

Post-review cleanups (no behavior change):
- backend-worker.ts: fix stale doc comment referencing the removed
  packages/api/browser/rpc.ts (the consumer is now index.browser.ts).
- api-browser-worker.ts: drop the redundant `endsWith('/data-file-index.txt')`
  disjunct (subsumed by `endsWith('data-file-index.txt')`) and the no-op
  `input as RequestInfo | URL` re-casts on the fetch pass-throughs.
- index.browser.ts: replace the single-field `state` holder with a plain
  module-level `let backend`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] api: drop the createBackendWorker wrapper, revert browser-preload churn

Now that the facade rides on loot-core's platform/client/connection (which
already ignores absurd-sql's `__`-prefixed internal messages), the only thing
the main thread still needs is absurd's one-line initBackend. So:

- Replace the 60-line createBackendWorker wrapper (onMessage/postMessage/
  terminate/listener filtering) with a tiny initBrowserBackend(worker) helper.
- Revert browser-preload/start.ts and worker-bridge.ts to their merged state —
  they keep importing absurd's initBackend directly, so this PR no longer
  refactors just-merged files and the loot-core change is purely additive.
- Facade holds a plain Worker handle and calls worker.terminate() on shutdown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] api: trim verbose comments in the browser build

Keep only the load-bearing "why" notes (the absurd-sql init, the fetch/.js.data
workaround, the global.Actual shim, the api-browser condition); drop protocol
diagrams, usage examples, and restated context. No code/behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] api: drop redundant absurd-sql dependency

No api source imports absurd-sql; it's reached only through @actual-app/core
(the facade's initBrowserBackend and the bundled worker), which declares it.
It's bundled into the build output, so it isn't a runtime dependency of the
published package either. Bundles are byte-for-byte unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] api: drop the api-browser condition, inject the worker into the connection

Review feedback: the api-browser export condition fought loot-core's idiom
(browser IS the default platform; node/"api" is the override) and hid a bug —
the condition-swapped lib only had `send`, so api.utils.amountToInteger was
silently undefined in the browser build.

Replace the condition with a plain transport seam:
- methods.ts no longer imports server/main; it sends through api/send.ts,
  which the Node entry wires to in-process lib.send and the browser entry
  wires to loot-core's client connection. utils.ts re-exports
  amountToInteger/integerToAmount from shared/util directly (fixes the bug).
- loot-core's client connection init() now accepts an optional injected
  socket; connectBackendWorker(worker) installs absurd-sql's bridge and
  connects in one call. This removes the forged global.Actual shim and the
  facade's `define: { global: 'globalThis' }` workaround. Desktop's argless
  init() path is unchanged.
- Delete main.api-browser.ts, the "./server/main" conditional export, and
  the facade's resolve.conditions.

The 4 loot-core snapshot failures (main.test.ts / transfer.test.ts) are
pre-existing on the branch without these changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] api: route methods through the client connection, slim the facade

Review feedback: get rid of the _setSend seam and move the worker init logic
into loot-core.

- methods.ts now imports `send` from loot-core's client connection. The node
  build resolves it via the existing `api` condition to a new in-process
  implementation (platform/client/connection/index.api.ts — previously a dead
  re-export of the electron client), matching the platform idiom: browser is
  the default, node/"api" overrides. api/send.ts is deleted and the node
  entry (index.ts) reverts to master byte-for-byte.
- loot-core's startBackendWorker(worker, config, assetsBaseUrl) now owns the
  whole worker boot (absurd-sql bridge + connection.init + server init RPC),
  so the browser entry just spawns the worker and calls it.
- Trims: single chained build script (drops the npm-run-all dep), copyFileSync
  instead of the linkOrCopy helper, drop the worker entry's error listeners
  (browsers already log worker uncaughts to DevTools).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] api: trim prose comments to the essential ones

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] fix client connection stalling when no sends are queued at connect

Found while smoke-testing the browser api build in a real consumer app: the
client connection only nulled messageQueue (switching from queueing to direct
postMessage) when the queue was non-empty at 'connect'. The desktop app always
has queued sends by then, so it never hits this; the api facade connects
before sending anything, so every later send queued forever and init() hung.
Drop the queue on connect unconditionally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] fix client connection dropping api/* error replies

Found via the browser smoke test: opening an e2e-encrypted budget without the
key surfaced as "undefined is not an object" instead of an error. The server
connection forwards api/* handler failures as {type:'reply', id, error}, but
the web client connection only read `result`, silently resolving undefined for
every failed api/* call. Reject when the reply carries an error, matching the
server's envelope (and the old api rpc behavior). The electron client has the
same latent gap; left untouched here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] api: move the worker entry back into the api package

Review feedback: browser-worker.ts is the api package's build entry, not
generic loot-core code — move it back (it now imports loot-core through the
public @actual-app/core exports instead of #-subpaths).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [AI] api: drop the manifest rewrite, align shutdown, trim comments

The data-file-index.txt manifest now lists migrations under their real .js
names (the worker's fetch shim already redirects those to the .js.data files
on disk), so the manifest-rewriting half of the shim is gone. The worker init
payload carries config and assetsBaseUrl as separate fields instead of
smuggling a private key into the config object, browser shutdown() mirrors
the node facade, and the client connection's queue flush drops a redundant
branch.

* [AI] fix rejected handler promises poisoning close-budget

runHandler only removed a method's promise from runningMethods when it
fulfilled, so any handler rejection left a stale promise in the set. The next
close-budget then failed: flushRunningMethods' Promise.all rejected with that
old, unrelated error before closeBudget even ran. Remove settled promises on
rejection too, and flush with Promise.allSettled since rejected methods
already reported their error to their caller.

* [AI] api: add a browser e2e test

A Playwright test loads the built browser bundle in chromium from a static
COOP/COEP server and exercises the full stack: worker boot, the rejected-error
contract, budget import, transaction read-back, and IndexedDB persistence
across a page reload. A dedicated workflow runs it in CI on api/loot-core
changes.

* [AI] api: drop the worker fetch shim; strip .data names in loot-core fs

The .js.data scheme was spread across three coordinated pieces: the build
renamed migrations on disk, the manifest listed names that didn't exist, and
a global fetch monkey-patch in the worker un-lied at runtime. Now the
manifest lists the true on-disk names and populateDefaultFilesystem (the only
fetcher of these files) strips the .data suffix when naming the virtual file
— the fetch patch is gone and the suffix becomes a small documented fs
convention. Also: e2e static server uses a single stat and drops a dead MIME
entry.

* [AI] fix electron client connection dropping api/* error replies

Both server connection variants post api/* handler failures as
{type: 'reply', id, error}, but only the web client rejected on it
(778ed25cf); the electron renderer client still resolved undefined. Mirror
the check so the wire contract is implemented uniformly.

* [AI] api: exclude the playwright e2e from vitest; review polish

vitest's default include matched e2e/browser.test.ts and failed CI - a
playwright test file defines no vitest tests. Exclude e2e/ from the unit-test
run (it has its own yarn e2e script). Also from review: the browser exports
condition now carries its own declaration file (the browser init's return
type differs from the node facade), and a failed init terminates the worker
it just spawned before rethrowing instead of leaving it running.

* [AI] api: trim code comments to the essentials

* [AI] api: always terminate the worker when browser shutdown fails

Review feedback: a rejected close-budget left the worker running. Terminate
in a finally block so cleanup is guaranteed while the failure still reaches
the caller.

* [AI] docs: document experimental browser support in the api package

* [AI] docs: drop the CORS note from the api browser section

* [AI] docs: trim the api browser section to the basics

* [AI] api: drop the import comment in methods.ts

* [AI] Run API browser test on depot runner

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [AI] Align api vite-plugin-node-polyfills to 0.28.0

The api workspace was pinned to ^0.27.0 (left over from an earlier merge
conflict resolution) while loot-core uses ^0.28.0. This both tripped the
workspace version-consistency constraint and broke the browser worker build:
under vite 8.0.16 the 0.27 process shim no longer round-trips runtime writes
to process.env.PUBLIC_URL, so sql-wasm.wasm was fetched from
/dist/undefinedsql-wasm.wasm (404) and api.init() failed in the browser e2e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 06:32:26 +00:00