actual-mcp-server 0.22.46 → 0.22.47

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
@@ -836,7 +836,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
836
836
 
837
837
  ---
838
838
 
839
- **Version:** 0.22.46 | **Tool Count:** 84 (verified LibreChat-compatible)
839
+ **Version:** 0.22.47 | **Tool Count:** 84 (verified LibreChat-compatible)
840
840
 
841
841
  ## Sponsor
842
842
 
@@ -40,7 +40,10 @@
40
40
  // point runs `_enforceBudgetAcl`, which refuses when the request context has no
41
41
  // allowedBudgets. Resolving the ACL would therefore be blocked by the ACL it is
42
42
  // trying to compute. We break that circularity by running ONLY this one call
43
- // inside `requestContext.run({ allowedBudgets: ['*'] }, ...)`.
43
+ // inside `requestContext.run({ allowedBudgets: ['*'] }, ...)`. Since #570 the
44
+ // wildcard matters twice: `getBudgets()` also FILTERS its result to the context's
45
+ // allowedBudgets, so without it the resolver would see only the files the
46
+ // caller's (not yet computed) list already permits.
44
47
  //
45
48
  // That is sound rather than a loophole: `getBudgets()` lists budget FILES
46
49
  // (name, id, who may access them). It reads no budget CONTENTS: no accounts, no
@@ -379,9 +379,7 @@ function abortBatchOnInfrastructureError(error, ctx) {
379
379
  * Claude Desktop / Claude Code compatibility.
380
380
  */
381
381
  function _enforceBudgetAcl(toolName) {
382
- const store = requestContext.getStore();
383
- const sessionId = store?.sessionId;
384
- const allowedBudgets = store?.allowedBudgets;
382
+ const sessionId = requestContext.getStore()?.sessionId;
385
383
  // Trusted-local short-circuit. stdio and startup health checks run with no
386
384
  // sessionId; in non-OIDC modes those are by-construction trusted (single
387
385
  // user, local process). The ACL only applies when an authenticated multi-
@@ -389,20 +387,8 @@ function _enforceBudgetAcl(toolName) {
389
387
  if (!sessionId && config.AUTH_PROVIDER !== 'oidc') {
390
388
  return;
391
389
  }
392
- // OIDC + no allowedBudgets in context: the request slipped past the
393
- // middleware. Defence-in-depth: refuse rather than fail open.
394
- if (config.AUTH_PROVIDER === 'oidc' && !allowedBudgets) {
395
- logger.warn(JSON.stringify({
396
- event: 'acl_denied',
397
- reason: 'no_allowed_budgets_in_context',
398
- sessionId: sessionId ?? null,
399
- tool: toolName ?? null,
400
- }));
401
- throw new Error('Budget ACL: no allowedBudgets in request context. ' +
402
- 'This request bypassed the budget-acl middleware. Refusing for safety. See #156.');
403
- }
404
- // No restriction.
405
- if (!allowedBudgets || allowedBudgets.includes('*'))
390
+ const allowedBudgets = restrictedAllowedBudgets(toolName);
391
+ if (!allowedBudgets)
406
392
  return;
407
393
  const target = getActiveBudgetConfig();
408
394
  if (!allowedBudgets.includes(target.syncId)) {
@@ -414,9 +400,46 @@ function _enforceBudgetAcl(toolName) {
414
400
  sessionId: sessionId ?? null,
415
401
  tool: toolName ?? null,
416
402
  }));
417
- throw new Error(`Budget ACL: budget "${target.name}" (${target.syncId}) is not in this session's allowedBudgets.`);
403
+ // #570: the name and syncId stay in the log above, never in the caller-facing
404
+ // text. With no selection the active budget is the registry's first entry, so
405
+ // naming it told a principal allowed only another budget what the default was.
406
+ throw new Error("Budget ACL: the active budget is not in this session's allowedBudgets. " +
407
+ 'Call actual_budgets_list_available and switch to a permitted budget.');
418
408
  }
419
409
  }
410
+ /**
411
+ * The request's budget allow-list when the caller is RESTRICTED, else `undefined` (#570).
412
+ *
413
+ * Shared by `_enforceBudgetAcl` (the active budget) and the paths that check nothing
414
+ * about the active budget: the two listings, the import and the switch's not-found
415
+ * list. One decision, so the two kinds of check cannot drift apart.
416
+ *
417
+ * - `AUTH_PROVIDER=oidc` with no `allowedBudgets` in context: the request slipped
418
+ * past the middleware, so throw (fail closed), with an `acl_denied` warn.
419
+ * - a list containing `"*"`, or no list outside OIDC (bearer, no-auth, stdio, CLI,
420
+ * startup): unrestricted.
421
+ * - otherwise: the list.
422
+ *
423
+ * Keyed on the request's allow-list, not on AUTH_PROVIDER, so any auth mode that
424
+ * attaches a list is restricted by it.
425
+ */
426
+ function restrictedAllowedBudgets(toolName) {
427
+ const store = requestContext.getStore();
428
+ const allowedBudgets = store?.allowedBudgets;
429
+ if (config.AUTH_PROVIDER === 'oidc' && !allowedBudgets) {
430
+ logger.warn(JSON.stringify({
431
+ event: 'acl_denied',
432
+ reason: 'no_allowed_budgets_in_context',
433
+ sessionId: store?.sessionId ?? null,
434
+ tool: toolName ?? null,
435
+ }));
436
+ throw new Error('Budget ACL: no allowedBudgets in request context. ' +
437
+ 'This request bypassed the budget-acl middleware. Refusing for safety. See #156.');
438
+ }
439
+ if (!allowedBudgets || allowedBudgets.includes('*'))
440
+ return undefined;
441
+ return allowedBudgets;
442
+ }
420
443
  /**
421
444
  * Helper to run an operation with the Actual API ready, deciding the lifecycle
422
445
  * mode automatically:
@@ -3862,7 +3885,18 @@ export async function getBudgets() {
3862
3885
  return withActualApi(async () => {
3863
3886
  observability.incrementToolCall('actual.budgets.getAll').catch(() => { });
3864
3887
  const raw = await withConcurrency(() => retry(() => rawGetBudgets(), { retries: 2, backoffMs: 200 }));
3865
- return Array.isArray(raw) ? raw : [];
3888
+ const files = Array.isArray(raw) ? raw : [];
3889
+ // #570: a restricted caller sees only files whose groupId (the sync id) it may
3890
+ // use. A file with no groupId was never uploaded and belongs to no list. The
3891
+ // dynamic ACL resolver calls this under its own `allowedBudgets: ['*']`, so it
3892
+ // still sees every file.
3893
+ const allowed = restrictedAllowedBudgets('actual_budgets_get_all');
3894
+ if (!allowed)
3895
+ return files;
3896
+ return files.filter(f => {
3897
+ const groupId = f?.groupId;
3898
+ return typeof groupId === 'string' && allowed.includes(groupId);
3899
+ });
3866
3900
  });
3867
3901
  }
3868
3902
  /**
@@ -3899,12 +3933,18 @@ export async function switchBudget(name) {
3899
3933
  }
3900
3934
  const key = name.toLowerCase();
3901
3935
  const found = budgetRegistry.get(key);
3902
- if (!found) {
3903
- const available = [...budgetRegistry.values()].map(b => `"${b.name}"`).join(', ');
3904
- throw new Error(`Budget "${name}" not found in configuration. ` +
3936
+ // #570: a restricted caller is told only about budgets it may use.
3937
+ const notFound = () => {
3938
+ const permitted = restrictedAllowedBudgets('actual_budgets_switch');
3939
+ const available = [...budgetRegistry.values()]
3940
+ .filter(b => !permitted || permitted.includes(b.syncId))
3941
+ .map(b => `"${b.name}"`).join(', ') || 'none permitted for this session';
3942
+ return new Error(`Budget "${name}" not found in configuration. ` +
3905
3943
  `Available budgets: ${available}. ` +
3906
3944
  `Add BUDGET_n_NAME/SYNC_ID/SERVER_URL vars to configure additional budgets.`);
3907
- }
3945
+ };
3946
+ if (!found)
3947
+ throw notFound();
3908
3948
  // ACL enforcement: the target budget must be in this session's allowedBudgets.
3909
3949
  // OIDC mode: explicit ACL required. Non-OIDC: short-circuit allow (single-user).
3910
3950
  if (config.AUTH_PROVIDER === 'oidc') {
@@ -3926,7 +3966,12 @@ export async function switchBudget(name) {
3926
3966
  sessionId,
3927
3967
  tool: 'actual_budgets_switch',
3928
3968
  }));
3929
- throw new Error(`Budget ACL: budget "${found.name}" (${found.syncId}) is not in this session's allowedBudgets.`);
3969
+ // #570: a denied budget gets the SAME error as an unknown name, so guessing
3970
+ // names cannot confirm that a budget outside the list exists. The round 1
3971
+ // review accepted a distinct "Budget ACL:" refusal here; that was reversed
3972
+ // because hiding other budgets is the point of this ticket. The log above
3973
+ // keeps the syncId for the operator.
3974
+ throw notFound();
3930
3975
  }
3931
3976
  }
3932
3977
  // #189 Phase 1: the principal's chosen budget is persisted at each commit
@@ -4033,10 +4078,13 @@ export function clearSessionBudgetState(sessionId) {
4033
4078
  sessionBudgetState.delete(sessionId);
4034
4079
  }
4035
4080
  /**
4036
- * Return all configured budgets from the registry (for listing in actual_budgets_list_available).
4081
+ * Return the configured budgets this caller may use (for actual_budgets_list_available).
4082
+ * A restricted caller sees only its permitted entries (#570); the startup registry log
4083
+ * reads the Map directly and is not filtered.
4037
4084
  */
4038
4085
  export function getBudgetRegistry() {
4039
- return [...budgetRegistry.values()].map(b => ({
4086
+ const allowed = restrictedAllowedBudgets('actual_budgets_list_available');
4087
+ return [...budgetRegistry.values()].filter(b => !allowed || allowed.includes(b.syncId)).map(b => ({
4040
4088
  name: b.name,
4041
4089
  syncId: b.syncId,
4042
4090
  serverUrl: b.serverUrl,
@@ -4237,11 +4285,28 @@ export async function exportBudget() {
4237
4285
  * Side effect worth knowing at every call site: upstream `importBudget` LOADS the
4238
4286
  * imported budget, so the session's active budget changes to the new file. It is
4239
4287
  * therefore outside the `BUDGET_N_*` registry and outside whatever budget the ACL
4240
- * resolved for this request. The write queue still enforces the ACL on entry, so a
4241
- * caller cannot use this to escape a budget they were already denied, but the
4242
- * resulting budget is un-ACL'd because it did not exist when the ACL was built.
4288
+ * resolved for this request. The resulting budget is un-ACL'd because it did not
4289
+ * exist when the ACL was built, so a RESTRICTED caller is refused outright (#570),
4290
+ * before any lock or upstream call. Wildcard and unrestricted callers can still
4291
+ * import; the write queue also enforces the ACL on their active budget.
4292
+ *
4293
+ * Upstream creates a NEW cloud file for the import (fresh id, best-effort upload),
4294
+ * so it never overwrites another budget on the server. The local copy is keyed by
4295
+ * the archive's own id, so an archive of budget X replaces X's LOCAL copy.
4243
4296
  */
4244
4297
  export async function importBudget(input, opts = {}) {
4298
+ const restricted = restrictedAllowedBudgets('actual_budgets_import');
4299
+ if (restricted) {
4300
+ logger.warn(JSON.stringify({
4301
+ event: 'acl_denied',
4302
+ reason: 'import_by_restricted_principal',
4303
+ allowedBudgets: restricted,
4304
+ sessionId: requestContext.getStore()?.sessionId ?? null,
4305
+ tool: 'actual_budgets_import',
4306
+ }));
4307
+ throw new Error('Budget ACL: actual_budgets_import is not available to a session with a restricted ' +
4308
+ 'allowedBudgets list: the imported budget would belong to no permitted list.');
4309
+ }
4245
4310
  observability.incrementToolCall('actual.budgets.import').catch(() => { });
4246
4311
  const result = await queueWriteOperation(async () => {
4247
4312
  // #394: an import re-points the process-global singleton exactly as a download does, so it
@@ -3,7 +3,7 @@ import adapter from '../lib/actual-adapter.js';
3
3
  const InputSchema = z.object({});
4
4
  const tool = {
5
5
  name: 'actual_budgets_get_all',
6
- description: 'Get a list of all available budget files. Useful for multi-budget management and discovering available budgets on the server.',
6
+ description: 'Get the budget files on the server that this session may use. Useful for multi-budget management and discovering available budgets.',
7
7
  inputSchema: InputSchema,
8
8
  call: async (_args, _meta) => {
9
9
  const result = await adapter.getBudgets();
@@ -7,13 +7,12 @@ import adapter from '../lib/actual-adapter.js';
7
7
  * TWO SIDE EFFECTS EVERY CALLER MUST KNOW, both verified against `importActual` in
8
8
  * the installed `@actual-app/api` and against a live server in the Docker E2E run:
9
9
  *
10
- * 1. IT CAN OVERWRITE AN EXISTING BUDGET. The budget id is taken from the ARCHIVE,
11
- * not freshly generated. `importActual` closes the current budget, restores the
12
- * archive under the id the archive carries, and loads it. So re-importing an
13
- * export of budget X restores ONTO budget X, replacing that budget's local data.
14
- * This is what makes export/import a genuine backup-and-restore pair, and it is
15
- * also why an import against a budget with real data is destructive. It is only
16
- * "a new budget" when the archive's id is not already present.
10
+ * 1. IT CAN OVERWRITE AN EXISTING BUDGET'S LOCAL COPY. The local budget directory is
11
+ * keyed by the id the ARCHIVE carries, so re-importing an export of budget X
12
+ * replaces X's local data. On the SERVER it never overwrites anything (#570):
13
+ * `importActual` clears the cloud file and group ids and uploads the result as a
14
+ * NEW budget file (best effort; a failed upload leaves it local only).
15
+ * A session with a restricted budget ACL is refused this tool outright (#570).
17
16
  * 2. IT CHANGES THE ACTIVE BUDGET. The imported budget is loaded, so the session
18
17
  * switches to it. If the archive's id is not in the `BUDGET_N_*` registry, that
19
18
  * budget is also outside whatever set the ACL resolved for this request, and
@@ -66,7 +65,7 @@ export default createTool({
66
65
  name: 'actual_budgets_import',
67
66
  description: 'Restore or import a budget from an Actual Budget ".zip" export or a YNAB4/YNAB5 export. ' +
68
67
  'DESTRUCTIVE: the budget id comes from the archive, so re-importing an export REPLACES that ' +
69
- 'budget\'s data rather than creating a copy. Never call this to "have a look at" a file. Confirm ' +
68
+ 'budget\'s local copy, and the result is uploaded to the server as a new budget file. Never call this to "have a look at" a file. Confirm ' +
70
69
  'with the user before importing over a budget that holds real data. ' +
71
70
  'It also LOADS the imported budget, so the active budget for this session changes; if the archive\'s ' +
72
71
  'budget is not in the configured list, call actual_budgets_list_available and actual_budgets_switch ' +
@@ -3,7 +3,7 @@ import adapter from '../lib/actual-adapter.js';
3
3
  const InputSchema = z.object({});
4
4
  const tool = {
5
5
  name: 'actual_budgets_list_available',
6
- description: 'List all pre-configured budgets available for switching. ' +
6
+ description: 'List the pre-configured budgets this session may switch to (a session limited to some budgets sees only those). ' +
7
7
  'Each entry shows the budget name, sync ID, server URL, and whether it uses E2E encryption. ' +
8
8
  'Budgets are configured via BUDGET_n_NAME / BUDGET_n_SYNC_ID / BUDGET_n_SERVER_URL env vars. ' +
9
9
  'Pass the budget name to actual_budgets_switch to change the active budget.',
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.46",
4
+ "version": "0.22.47",
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_selection_precondition.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/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",