actual-mcp-server 0.7.13 → 0.8.1

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
@@ -473,6 +473,7 @@ All configuration is via environment variables. Copy `.env.example` to `.env` to
473
473
  | `OIDC_ALLOW_INSECURE_ISSUER` | `false` | No | Allow a plaintext (http) OIDC issuer on a trusted network (#244). Off by default (http issuer refused at startup); set `true` only for local/LAN testing |
474
474
  | `OIDC_RESOURCE` | _(none)_ | No | Expected `aud` claim in JWT (your client ID) |
475
475
  | `OIDC_ACCEPTED_AUDIENCES` | _(none)_ | No | Extra accepted `aud` values beyond `OIDC_RESOURCE`, comma-separated (#245). For IdPs that put the client-id in `aud` (e.g. Authentik). Strict allowlist, never a wildcard |
476
+ | `OIDC_JWKS_TRUSTED_HOSTS` | _(none)_ | No | Opt-in cross-origin JWKS hosts, comma-separated `host` or `host:port` (#254). For IdPs whose `jwks_uri` lives on another host, e.g. Google needs `www.googleapis.com`. Exact match, no wildcards; empty default keeps same-origin-only |
476
477
  | `OIDC_SCOPES` | _(none)_ | No | Comma-separated required scopes; leave empty for Casdoor |
477
478
  | `AUTH_BUDGET_ACL` | _(none)_ | No | Per-user budget ACL; see [AI Client Setup](docs/guides/AI_CLIENT_SETUP.md#oidc-authentication-multi-user) |
478
479
  | `MCP_ENABLE_HTTPS` | `false` | No | Enable native TLS. Requires `MCP_HTTPS_CERT` and `MCP_HTTPS_KEY` |
@@ -718,4 +719,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
718
719
 
719
720
  ---
720
721
 
721
- **Version:** 0.7.13 | **Tool Count:** 71 (verified LibreChat-compatible)
722
+ **Version:** 0.8.1 | **Tool Count:** 71 (verified LibreChat-compatible)
package/dist/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "actual-mcp-server",
3
3
  "displayName": "Actual MCP Server",
4
- "version": "0.7.13",
4
+ "version": "0.8.1",
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/generated_tools.smoke.test.js && node tests/unit/schema_validation.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/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.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/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/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/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/payees_delete.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/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/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.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/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/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js",
33
+ "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.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/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.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/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/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/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/payees_delete.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/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/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.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/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/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.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",
@@ -78,7 +78,7 @@
78
78
  "devDependencies": {
79
79
  "@playwright/test": "^1.60.0",
80
80
  "@types/express": "^5.0.3",
81
- "@types/node": "^25.9.2",
81
+ "@types/node": "^26.1.0",
82
82
  "knip": "^6.16.1",
83
83
  "node-fetch": "^3.3.2",
84
84
  "tsconfig-paths": "^4.2.0",
@@ -46,6 +46,11 @@ export const configSchema = z.object({
46
46
  // Strict closed allowlist: the accepted set is OIDC_RESOURCE plus these; never a
47
47
  // wildcard. Default empty, so single-audience deployments are unchanged.
48
48
  OIDC_ACCEPTED_AUDIENCES: z.string().optional(),
49
+ // #254: opt-in allowlist of cross-origin JWKS hosts, comma-separated `host` or
50
+ // `host:port` entries (e.g. "www.googleapis.com" for a Google issuer). Raw string
51
+ // here; parsed fail-fast by buildTrustedJwksHosts (oidc-discovery.ts) at the
52
+ // composition root, per the #245 pattern. Default empty: same-origin only.
53
+ OIDC_JWKS_TRUSTED_HOSTS: z.string().optional(),
49
54
  // Comma-separated required scopes (e.g. "read,write"). Optional.
50
55
  OIDC_SCOPES: z.string().optional(),
51
56
  // JSON map of principal → budget sync-ID list for per-user budget ACL.
@@ -14,12 +14,14 @@ import { createModuleLogger } from './loggerFactory.js';
14
14
  * - the resolved `jwks_uri` must be https and on the SAME host as the issuer, so
15
15
  * a tampered or misconfigured discovery document cannot point key fetching at
16
16
  * an attacker-controlled host. Cross-host issuers (for example Google, which
17
- * serves keys from googleapis.com) are intentionally not auto-trusted; that is
18
- * a separate, configurable follow-up (#245).
17
+ * serves keys from googleapis.com) are never auto-trusted; the operator can
18
+ * allowlist specific cross-origin JWKS hosts via OIDC_JWKS_TRUSTED_HOSTS
19
+ * (#254, opt-in, exact host:port match, https required unconditionally).
19
20
  *
20
- * The pure helpers (assertSecureIssuer, resolveJwksUri) are unit-tested without a
21
- * network; discoverJwksUri wraps them around a single startup fetch and fails
22
- * closed on any problem.
21
+ * The helpers (assertSecureIssuer, buildTrustedJwksHosts, resolveJwksUri) are
22
+ * network-free and unit-tested directly (resolveJwksUri logs on the allowlist
23
+ * accept path, otherwise side-effect free); discoverJwksUri wraps them around a
24
+ * single startup fetch and fails closed on any problem.
23
25
  */
24
26
  const logger = createModuleLogger('OIDC_DISCOVERY');
25
27
  const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
@@ -28,6 +30,47 @@ const DISCOVERY_TIMEOUT_MS = 10_000;
28
30
  export function isLoopbackHost(host) {
29
31
  return host ? LOOPBACK_HOSTS.has(host.toLowerCase()) : false;
30
32
  }
33
+ /**
34
+ * Parse OIDC_JWKS_TRUSTED_HOSTS (comma-separated `host` or `host:port`
35
+ * entries) into a normalized, deduplicated allowlist (#254). Mirrors the #245
36
+ * buildAcceptedAudiences pattern: config stays a raw string, parsing happens
37
+ * here, and the result is threaded as a parameter. Entries are trimmed,
38
+ * lowercased, and empties dropped, then validated by a URL round-trip: an
39
+ * entry must equal `new URL('https://' + entry).host`, which rejects in one
40
+ * rule everything that could never match a real jwks_uri host: schemes,
41
+ * paths, credentials, wildcards, whitespace, empty or non-numeric ports,
42
+ * default-port `:443` suffixes (URL.host elides them, so such an entry would
43
+ * be silently inert), raw-unicode IDN (URL.host is punycode), and unbracketed
44
+ * IPv6. THROWS on the first invalid entry so a misconfigured allowlist fails
45
+ * fast at startup instead of silently failing closed at the first
46
+ * cross-origin issuer.
47
+ */
48
+ export function buildTrustedJwksHosts(configured) {
49
+ if (!configured)
50
+ return [];
51
+ const entries = configured
52
+ .split(',')
53
+ .map((entry) => entry.trim().toLowerCase())
54
+ .filter(Boolean);
55
+ for (const entry of entries) {
56
+ let canonical;
57
+ try {
58
+ canonical = new URL(`https://${entry}`).host;
59
+ }
60
+ catch {
61
+ canonical = undefined;
62
+ }
63
+ // WHATWG URL does not forbid "*" in a hostname, so the round-trip alone
64
+ // would let a wildcard through; there is deliberately no wildcard matching.
65
+ if (entry.includes('*') || canonical !== entry) {
66
+ throw new Error(`OIDC_JWKS_TRUSTED_HOSTS entry is invalid: "${entry}". Entries must be canonical URL ` +
67
+ 'hosts: a bare lowercase hostname, optionally with a NON-default port (host:port; ' +
68
+ '":443" is elided by URL parsing and would never match), no scheme, path, ' +
69
+ 'credentials, or wildcards; IDN as punycode; IPv6 in brackets.');
70
+ }
71
+ }
72
+ return Array.from(new Set(entries));
73
+ }
31
74
  /**
32
75
  * Parse and validate the issuer URL. Throws unless it is https, or an explicit
33
76
  * loopback host (local dev). Returns the parsed URL.
@@ -62,11 +105,16 @@ export function assertSecureIssuer(issuer, allowInsecure = false) {
62
105
  return url;
63
106
  }
64
107
  /**
65
- * Pure: resolve and validate the `jwks_uri` from a parsed discovery document
66
- * against the issuer. Throws (fail closed) if it is missing, not https, carries
67
- * embedded credentials, or is not same-origin (scheme + host + port) with the issuer.
108
+ * Network-free: resolve and validate the `jwks_uri` from a parsed discovery
109
+ * document against the issuer. Throws (fail closed) if it is missing, not
110
+ * https, carries embedded credentials, or is not same-origin (scheme + host +
111
+ * port) with the issuer, UNLESS the jwks host is explicitly listed in
112
+ * `trustedHosts` (#254): an allowlisted cross-origin jwks_uri is accepted,
113
+ * must still be https (no OIDC_ALLOW_INSECURE_ISSUER or loopback exemption on
114
+ * the cross-origin path), and the acceptance is audit-logged (the one side
115
+ * effect of this function).
68
116
  */
69
- export function resolveJwksUri(discoveryDoc, issuer, allowInsecure = false) {
117
+ export function resolveJwksUri(discoveryDoc, issuer, allowInsecure = false, trustedHosts = []) {
70
118
  const issuerUrl = assertSecureIssuer(issuer, allowInsecure);
71
119
  if (!discoveryDoc || typeof discoveryDoc !== 'object') {
72
120
  throw new Error('OIDC discovery document is empty or not an object');
@@ -92,8 +140,34 @@ export function resolveJwksUri(discoveryDoc, issuer, allowInsecure = false) {
92
140
  // same host is a different service, so comparing origins keeps the guarantee the
93
141
  // security review asked for. Both are https here, so this is effectively host+port.
94
142
  if (jwks.origin.toLowerCase() !== issuerUrl.origin.toLowerCase()) {
95
- throw new Error(`OIDC "jwks_uri" origin (${jwks.origin}) does not match the issuer origin (${issuerUrl.origin}); ` +
96
- 'refusing a cross-origin key fetch. A cross-host issuer (e.g. Google) needs an explicit trusted-host allowlist (#245).');
143
+ // #254: opt-in escape hatch for legitimately cross-host IdPs (Google serves
144
+ // JWKS from www.googleapis.com). Matching is on URL.host (hostname PLUS any
145
+ // non-default port), exact and lowercased: a bare-host entry does not trust
146
+ // odd ports on that host, preserving this file's origin-level strictness.
147
+ // Empty allowlist (the default) makes this branch behave exactly as before.
148
+ const jwksHost = jwks.host.toLowerCase();
149
+ if (!trustedHosts.includes(jwksHost)) {
150
+ throw new Error(`OIDC "jwks_uri" origin (${jwks.origin}) does not match the issuer origin (${issuerUrl.origin}); ` +
151
+ `refusing a cross-origin key fetch. Checked host "${jwksHost}" against ` +
152
+ `OIDC_JWKS_TRUSTED_HOSTS [${trustedHosts.join(', ')}]; matching is exact on host:port ` +
153
+ '(#254). A cross-host issuer (e.g. Google) needs its JWKS host explicitly allowlisted.');
154
+ }
155
+ // An allowlisted CROSS-ORIGIN fetch must be https, with no exemption: not
156
+ // for OIDC_ALLOW_INSECURE_ISSUER (the #244 opt-out covers a same-origin LAN
157
+ // issuer, never third-party key hosts) and not for loopback (a shared-host
158
+ // process could bind the port). Without this, combining the two opt-ins
159
+ // would newly permit a plaintext cross-origin key fetch that pre-#254 code
160
+ // always refused.
161
+ if (jwks.protocol !== 'https:') {
162
+ throw new Error(`OIDC cross-origin "jwks_uri" allowed by OIDC_JWKS_TRUSTED_HOSTS must use https (got ${jwks.protocol}); ` +
163
+ 'the OIDC_ALLOW_INSECURE_ISSUER opt-out never extends to cross-origin key fetches (#254).');
164
+ }
165
+ // Audit trail: a normally-rejected cross-origin trust was relaxed by operator config.
166
+ logger.info('cross-origin JWKS host allowed by OIDC_JWKS_TRUSTED_HOSTS', {
167
+ issuerOrigin: issuerUrl.origin,
168
+ jwksHost,
169
+ jwksUri: jwks.toString(),
170
+ });
97
171
  }
98
172
  return jwks.toString();
99
173
  }
@@ -102,7 +176,7 @@ export function resolveJwksUri(discoveryDoc, issuer, allowInsecure = false) {
102
176
  * Fails closed on any problem (non-https issuer, redirect, non-200, bad JSON,
103
177
  * missing/invalid/cross-host jwks_uri). `fetchImpl` is injectable for testing.
104
178
  */
105
- export async function discoverJwksUri(issuer, allowInsecure = false, fetchImpl = fetch) {
179
+ export async function discoverJwksUri(issuer, allowInsecure = false, trustedHosts = [], fetchImpl = fetch) {
106
180
  assertSecureIssuer(issuer, allowInsecure);
107
181
  const base = issuer.endsWith('/') ? issuer.slice(0, -1) : issuer;
108
182
  const discoveryUrl = `${base}/.well-known/openid-configuration`;
@@ -126,7 +200,7 @@ export async function discoverJwksUri(issuer, allowInsecure = false, fetchImpl =
126
200
  catch {
127
201
  throw new Error(`OIDC discovery document at ${discoveryUrl} is not valid JSON`);
128
202
  }
129
- const jwksUri = resolveJwksUri(doc, issuer, allowInsecure);
203
+ const jwksUri = resolveJwksUri(doc, issuer, allowInsecure, trustedHosts);
130
204
  logger.info('Resolved JWKS URI from OIDC discovery', { jwksUri });
131
205
  return jwksUri;
132
206
  }
@@ -13,7 +13,7 @@ import config from '../config.js';
13
13
  import { createRemoteJWKSet, jwtVerify } from 'jose';
14
14
  import { MCPAuthTokenVerificationError } from 'mcp-auth';
15
15
  import { createMcpAuth } from '../auth/setup.js';
16
- import { discoverJwksUri } from '../lib/oidc-discovery.js';
16
+ import { discoverJwksUri, buildTrustedJwksHosts } from '../lib/oidc-discovery.js';
17
17
  import { buildAcceptedAudiences } from '../lib/oidc-audiences.js';
18
18
  import { budgetAclMiddleware } from '../auth/budget-acl.js';
19
19
  import * as https from 'node:https';
@@ -83,7 +83,10 @@ advertisedUrl) {
83
83
  // closed on a non-https issuer, a cross-origin redirect, a missing/invalid
84
84
  // jwks_uri, or a jwks_uri on a different host than the issuer. jose's
85
85
  // createRemoteJWKSet then fetches the keys lazily and caches them.
86
- const jwksUri = await discoverJwksUri(config.OIDC_ISSUER, config.OIDC_ALLOW_INSECURE_ISSUER);
86
+ // #254: opt-in cross-origin JWKS trusted hosts (e.g. Google serves keys from
87
+ // www.googleapis.com). Parsed fail-fast here at the composition root and
88
+ // threaded as a parameter; empty (the default) keeps same-origin-only.
89
+ const jwksUri = await discoverJwksUri(config.OIDC_ISSUER, config.OIDC_ALLOW_INSECURE_ISSUER, buildTrustedJwksHosts(config.OIDC_JWKS_TRUSTED_HOSTS));
87
90
  const jwks = createRemoteJWKSet(new URL(jwksUri));
88
91
  const customJwtVerify = async (token) => {
89
92
  // Enforce the audience claim (#160, OWASP A07). Without it, any
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.7.13",
4
+ "version": "0.8.1",
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/generated_tools.smoke.test.js && node tests/unit/schema_validation.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/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.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/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/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/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/payees_delete.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/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/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.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/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/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js",
33
+ "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.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/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.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/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/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/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/payees_delete.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/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/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.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/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/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.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",
@@ -78,7 +78,7 @@
78
78
  "devDependencies": {
79
79
  "@playwright/test": "^1.60.0",
80
80
  "@types/express": "^5.0.3",
81
- "@types/node": "^25.9.2",
81
+ "@types/node": "^26.1.0",
82
82
  "knip": "^6.16.1",
83
83
  "node-fetch": "^3.3.2",
84
84
  "tsconfig-paths": "^4.2.0",