actual-mcp-server 0.22.50 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -838,7 +838,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
838
838
 
839
839
  ---
840
840
 
841
- **Version:** 0.22.50 | **Tool Count:** 84 (verified LibreChat-compatible)
841
+ **Version:** 0.23.0 | **Tool Count:** 84 (verified LibreChat-compatible)
842
842
 
843
843
  ## Sponsor
844
844
 
@@ -1,6 +1,8 @@
1
1
  import api from '@actual-app/api';
2
2
  import { withOpTimeout } from './opTimeout.js';
3
3
  import { setApiLogin, setLoadedBudgetSyncId, registerBudgetLoad, loginsSpanServers } from './apiState.js';
4
+ import { createModuleLogger } from './loggerFactory.js';
5
+ const log = createModuleLogger('API-LOGIN');
4
6
  // Upstream's `init` returns its module-level handler object, the same one every time. Upstream
5
7
  // exports no other way to reach `close-budget`. Nothing can be open before the first init, which
6
8
  // is when this is still unset.
@@ -43,14 +45,15 @@ let upstreamLib = null;
43
45
  * after the caller has thrown and released the lock. A late close would close the budget the next
44
46
  * operation loaded, in any deployment, so it is always registered with the #393 abandoned-load
45
47
  * registry: every `withApiLock` acquisition settles it, bounded and failing closed, before it runs
46
- * anything. A late login is registered only when the registry names more than one login
47
- * (`loginsSpanServers`): there it could move the client to another server under the next
48
- * operation, while with one login it lands on the only server there is, and registering it would
49
- * replace #270's per-request recovery with a fail-closed wait on every operation. The close
48
+ * anything. With one login, upstream logs in itself and the login is not registered: it lands on
49
+ * the only server there is, and registering it would replace #270's per-request recovery with a
50
+ * fail-closed wait on every operation. With more than one (`loginsSpanServers`), a late login
51
+ * could move the client to another server, so #574 performs it as our own abortable fetch and
52
+ * registers only the credential-less init that follows (see `initOverFetchedLogin`). The close
50
53
  * has no such exemption: it waits on upstream's spreadsheet and services (not on running handlers), so a close
51
54
  * that stalls makes every session wait, bounded and failing closed, in a one-login deployment
52
- * too (the accepted #393 trade-off; making the close and login abortable is #574). Their late
53
- * success records nothing (the records were cleared in step 1 and `setApiLogin` is only reached by
55
+ * too (the accepted #393 trade-off; upstream's close takes no signal, so it stays unabortable).
56
+ * A late close, login or init records nothing (the records were cleared in step 1 and `setApiLogin` is only reached by
54
57
  * a caller still waiting), so the next operation logs in again.
55
58
  */
56
59
  export async function initApiLoggedIn(dataDir, login, label) {
@@ -61,15 +64,176 @@ export async function initApiLoggedIn(dataDir, login, label) {
61
64
  registerBudgetLoad(closing);
62
65
  await withOpTimeout(() => closing, `${label} (close outgoing budget)`);
63
66
  }
67
+ if (loginsSpanServers()) {
68
+ await initOverFetchedLogin(dataDir, login, label);
69
+ return;
70
+ }
64
71
  const loggingIn = api.init({
65
72
  dataDir,
66
73
  serverURL: login.serverUrl,
67
74
  password: login.password,
68
75
  });
69
- if (loginsSpanServers())
70
- registerBudgetLoad(loggingIn);
71
76
  const lib = await withOpTimeout(() => loggingIn, label);
72
77
  if (lib && typeof lib.send === 'function')
73
78
  upstreamLib = lib;
74
79
  setApiLogin(login);
75
80
  }
81
+ /**
82
+ * #574: the multi-login path. Upstream's sign-in takes no signal, so a stalled login could only
83
+ * be raced, and its late answer then pointed the client at its server and wrote its token under
84
+ * whatever operation ran next. Here the login is our own fetch, aborted at the bound, and the
85
+ * client is only touched after that fetch has answered in time:
86
+ * 1. the bounded login: the fetch and its body read, nothing else. `finally` aborts it, which
87
+ * is a no-op once it has answered. There is deliberately no `signal.aborted` check after
88
+ * it (round 1 of the gate asked for one, round 2 reversed it): the `finally` aborts on
89
+ * success too, so the check would fail every login, and a timeout rejects the awaited race
90
+ * before the next line runs, which `multi_server_budget_selection.test.js` row 22 pins
91
+ * with a fetch that ignores the abort.
92
+ * 2. a credential-less `api.init` (local: `setServer` plus a fresh in-memory store), then
93
+ * `subscribe-set-token` on the handle it returns. Neither makes a network call, but both
94
+ * are still raced, so both are registered like the close: a late one would re-point the
95
+ * client or hand it the wrong server's token.
96
+ *
97
+ * An empty password keeps upstream's skip: no fetch and no token. Nothing of the previous server
98
+ * survives, because every `api.init` resets upstream's store (`init$3({ persist: false })`).
99
+ */
100
+ async function initOverFetchedLogin(dataDir, login, label) {
101
+ let token = '';
102
+ if (login.password) {
103
+ const controller = new AbortController();
104
+ try {
105
+ token = await withOpTimeout(() => loginOverFetch(login, controller.signal), label);
106
+ }
107
+ finally {
108
+ controller.abort();
109
+ }
110
+ }
111
+ // `password: ''` is upstream's no-credential init (it signs in only on a truthy password).
112
+ const pointing = api.init({ dataDir, serverURL: login.serverUrl, password: '' });
113
+ registerBudgetLoad(pointing);
114
+ const lib = await withOpTimeout(() => pointing, label);
115
+ if (!lib || typeof lib.send !== 'function')
116
+ throw new Error(`Actual API ${label}: upstream init returned no handle`);
117
+ upstreamLib = lib;
118
+ if (token) {
119
+ // Local, but raced and registered like the init: a late token set after the next init would
120
+ // hand this server's token to whichever server the client then points at.
121
+ const setting = lib.send('subscribe-set-token', { token });
122
+ registerBudgetLoad(setting);
123
+ await withOpTimeout(() => setting, label);
124
+ }
125
+ setApiLogin(login);
126
+ }
127
+ function authError(reason) {
128
+ return Object.assign(new Error(`Authentication failed: ${reason}`), { code: reason });
129
+ }
130
+ /** The server URL as written into a message: no `user:pass@`, which an operator may put there. */
131
+ function displayUrl(serverUrl) {
132
+ try {
133
+ const u = new URL(serverUrl);
134
+ u.username = '';
135
+ u.password = '';
136
+ return u.toString().replace(/\/$/, '');
137
+ }
138
+ catch {
139
+ return 'the configured server';
140
+ }
141
+ }
142
+ /**
143
+ * #574: upstream's password login (`signIn` over `post`), as our own fetch so it can be aborted.
144
+ * Resolves the session token. Errors follow upstream's `post`/`throwIfNot200` order and carry
145
+ * `.code` like its `withErrorCode`, because `classifyInitFailure` and `isRetryableAuthError`
146
+ * read them: in particular a 429 keeps upstream's raw text (`Too many requests, ...`), which is
147
+ * what the auth-rate-limit retry matches. Three cases go beyond upstream on purpose:
148
+ * `redirect-refused` (upstream follows a redirect and re-POSTs the password to its target),
149
+ * `invalid-server-url`, and `openid-required` (upstream returns `{ redirectUrl }` as a success,
150
+ * which a server-side password login cannot follow).
151
+ *
152
+ * Reads `globalThis.fetch` at call time; upstream captured its own at module load. Never logs
153
+ * or throws the password, the token or a response body.
154
+ */
155
+ export async function loginOverFetch(login, signal) {
156
+ let url;
157
+ try {
158
+ url = new URL(login.serverUrl);
159
+ }
160
+ catch {
161
+ throw authError('invalid-server-url');
162
+ }
163
+ url.pathname = `${url.pathname.replace(/\/+$/, '')}/account/login`;
164
+ let res;
165
+ let text;
166
+ try {
167
+ res = await globalThis.fetch(url.toString(), {
168
+ method: 'POST',
169
+ body: JSON.stringify({ loginMethod: 'password', password: login.password }),
170
+ headers: { 'Content-Type': 'application/json' },
171
+ // 'manual', not 'error': under 'error' Node rejects a redirect with the same
172
+ // `TypeError: fetch failed` as a refused connection, which would map to the retried
173
+ // `network-failure`. Here the 3xx arrives as a status and is refused below.
174
+ redirect: 'manual',
175
+ signal,
176
+ });
177
+ text = await res.text();
178
+ }
179
+ catch (err) {
180
+ if (signal.aborted)
181
+ throw authError('aborted');
182
+ log.debug('Login request failed', { error: err instanceof Error ? err.name : 'unknown' });
183
+ throw authError('network-failure');
184
+ }
185
+ if (res.status >= 300 && res.status < 400) {
186
+ // Refused even for an http to https upgrade on the same host (the usual TLS proxy setup):
187
+ // following any redirect re-POSTs the password, and one rule is easier to trust than a
188
+ // same-host exception. The operator fixes it once, so the message names the server URL the
189
+ // redirect implies: its origin plus the path before `/account/login` (a subpath deployment
190
+ // keeps its subpath), never its userinfo or query. No hint when the Location is absent,
191
+ // unparseable, not http(s), or implies the URL already configured.
192
+ let hint = '';
193
+ const location = res.headers.get('Location');
194
+ try {
195
+ const target = new URL(location ?? '', url);
196
+ const suggested = target.origin + target.pathname.replace(/\/account\/login\/*$/, '').replace(/\/+$/, '');
197
+ if (location && /^https?:$/.test(target.protocol) && suggested !== displayUrl(login.serverUrl)) {
198
+ hint = ` (it redirects to ${suggested}; configure that URL instead)`;
199
+ }
200
+ }
201
+ catch {
202
+ // An unparseable Location gives no hint; the refusal stands.
203
+ }
204
+ throw Object.assign(new Error(`Authentication failed: redirect refused by ${displayUrl(login.serverUrl)}${hint}`), { code: 'redirect-refused' });
205
+ }
206
+ if (res.status !== 200) {
207
+ if (res.status === 500)
208
+ throw authError('internal');
209
+ if ((res.headers.get('Content-Type') ?? '').toLowerCase().includes('application/json')) {
210
+ let body;
211
+ try {
212
+ body = JSON.parse(text);
213
+ }
214
+ catch {
215
+ throw authError('parse-json');
216
+ }
217
+ const reason = body?.reason === 'unauthorized' && body?.details === 'token-not-found' ? 'token-expired' : body?.reason;
218
+ throw authError(reason || 'network-failure');
219
+ }
220
+ if (res.headers.has('ngrok-error-code'))
221
+ throw authError('network-failure');
222
+ throw authError(text.slice(0, 200) || 'network-failure');
223
+ }
224
+ let data;
225
+ try {
226
+ data = JSON.parse(text);
227
+ }
228
+ catch {
229
+ throw authError('parse-json');
230
+ }
231
+ if (data?.status !== 'ok')
232
+ throw authError(data?.description || data?.reason || 'unknown');
233
+ if (data.data?.returnUrl) {
234
+ throw Object.assign(new Error(`Authentication failed: ${displayUrl(login.serverUrl)} requires OpenID login, which a server password cannot use`), { code: 'openid-required' });
235
+ }
236
+ if (!data.data?.token)
237
+ throw new Error('login: User token not set');
238
+ return data.data.token;
239
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "actual-mcp-server",
3
3
  "displayName": "Actual MCP Server",
4
- "version": "0.22.50",
4
+ "version": "0.23.0",
5
5
  "engines": {
6
6
  "node": ">=22.0.0",
7
7
  "npm": ">=10.0.0"
@@ -30,7 +30,7 @@
30
30
  "verify-tools": "npm run build && node scripts/verify-tools.js",
31
31
  "check:coverage": "node scripts/list-actual-api-methods.mjs",
32
32
  "direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
33
- "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/session_ownership.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_acl_listing.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/multi_server_budget_selection.test.js && node tests/unit/static_tokens.test.js && node tests/unit/httpServer_static_tokens.test.js && node tests/unit/multi_server_budget_selection_stdio.test.js && node tests/unit/multi_server_session_open_http.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/transactions_update_split_convert.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/tool_content_logging.test.js && node tests/unit/adapter_error_logging.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/session_budget_state_cleanup.test.js && node tests/unit/http_session_delete.test.js && node tests/unit/budget_acl_parse.test.js && node tests/unit/server_instructions.test.js && node tests/unit/query_run_empty_hint.test.js",
33
+ "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/session_ownership.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_acl_listing.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/multi_server_budget_selection.test.js && node tests/unit/static_tokens.test.js && node tests/unit/httpServer_static_tokens.test.js && node tests/unit/multi_server_budget_selection_stdio.test.js && node tests/unit/api_login_fetch.test.js && node tests/unit/multi_server_session_open_http.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/transactions_update_split_convert.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/tool_content_logging.test.js && node tests/unit/adapter_error_logging.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/session_budget_state_cleanup.test.js && node tests/unit/http_session_delete.test.js && node tests/unit/budget_acl_parse.test.js && node tests/unit/server_instructions.test.js && node tests/unit/query_run_empty_hint.test.js",
34
34
  "test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
35
35
  "test:e2e": "npx playwright test",
36
36
  "test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",