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.
|
|
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
|
|
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
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
4241
|
-
*
|
|
4242
|
-
*
|
|
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
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
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",
|