actual-mcp-server 0.6.30 → 0.6.32
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
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.6.
|
|
4
|
+
"version": "0.6.32",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.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_insecure_upstream.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/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_body_limit.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.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_insecure_upstream.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_body_limit.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",
|
|
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",
|
|
@@ -15,6 +15,7 @@ import retry, { isRetryableError } from './retry.js';
|
|
|
15
15
|
import logger from '../logger.js';
|
|
16
16
|
import config from '../config.js';
|
|
17
17
|
import { parseBudgetRegistry } from './budget-registry.js';
|
|
18
|
+
import { getPreferredBudgetSyncId, setPreferredBudgetSyncId, pickAllowedPreferredBudget } from './budget-preference-store.js';
|
|
18
19
|
import { requestContext } from './requestContext.js';
|
|
19
20
|
import { connectionPool } from './ActualConnectionPool.js';
|
|
20
21
|
import { isApiInitialized, setApiInitialized } from './apiState.js';
|
|
@@ -54,7 +55,8 @@ function getActiveBudgetConfig() {
|
|
|
54
55
|
// call works at runtime. If we're not in any requestContext.run scope (stdio,
|
|
55
56
|
// startup health checks), sessionId is undefined and we fall back to the
|
|
56
57
|
// env-default budget (first registry entry).
|
|
57
|
-
const
|
|
58
|
+
const store = requestContext.getStore();
|
|
59
|
+
const sessionId = store?.sessionId;
|
|
58
60
|
if (sessionId) {
|
|
59
61
|
const key = sessionBudgetState.get(sessionId);
|
|
60
62
|
if (key) {
|
|
@@ -62,6 +64,16 @@ function getActiveBudgetConfig() {
|
|
|
62
64
|
if (found)
|
|
63
65
|
return found;
|
|
64
66
|
}
|
|
67
|
+
// #189 Phase 1: no in-session selection yet (e.g. a fresh session after a
|
|
68
|
+
// server restart + client re-initialize). Restore the principal's persisted
|
|
69
|
+
// budget, but ONLY if the live ACL still permits it (pickAllowedPreferredBudget
|
|
70
|
+
// re-checks allowedBudgets, so a stale preference can never widen access).
|
|
71
|
+
// Memoize into the session slot so subsequent calls take the fast path above.
|
|
72
|
+
const restored = pickAllowedPreferredBudget(getPreferredBudgetSyncId(store?.principal), store?.allowedBudgets, [...budgetRegistry.values()]);
|
|
73
|
+
if (restored) {
|
|
74
|
+
sessionBudgetState.set(sessionId, restored.name.toLowerCase());
|
|
75
|
+
return restored;
|
|
76
|
+
}
|
|
65
77
|
}
|
|
66
78
|
return [...budgetRegistry.values()][0];
|
|
67
79
|
}
|
|
@@ -1607,6 +1619,10 @@ export async function switchBudget(name) {
|
|
|
1607
1619
|
throw new Error(`Budget ACL: budget "${found.name}" (${found.syncId}) is not in this session's allowedBudgets.`);
|
|
1608
1620
|
}
|
|
1609
1621
|
}
|
|
1622
|
+
// #189 Phase 1: the principal's chosen budget is persisted at each commit
|
|
1623
|
+
// point below (paired with the sessionBudgetState write), NOT here, so a
|
|
1624
|
+
// switch that throws before committing never leaves a stale preference. The
|
|
1625
|
+
// helper is keyed by a hash of the principal and never throws.
|
|
1610
1626
|
// Fast path (#172): if the current pool entry's auth descriptor matches the
|
|
1611
1627
|
// target budget's (same serverUrl + password + encryptionPassword), skip
|
|
1612
1628
|
// release + re-auth. Just download the new budget file on the already-
|
|
@@ -1620,6 +1636,7 @@ export async function switchBudget(name) {
|
|
|
1620
1636
|
if (sameAuth && currentEntry.syncId === found.syncId) {
|
|
1621
1637
|
// No-op: already on this exact budget. Keep session map consistent and return.
|
|
1622
1638
|
sessionBudgetState.set(sessionId, key);
|
|
1639
|
+
setPreferredBudgetSyncId(store?.principal, found.syncId); // #189: persist at commit
|
|
1623
1640
|
logger.info(`[ADAPTER] switchBudget no-op for session ${sessionId}: already on "${found.name}" (${found.syncId})`);
|
|
1624
1641
|
return { name: found.name, syncId: found.syncId, serverUrl: found.serverUrl };
|
|
1625
1642
|
}
|
|
@@ -1644,6 +1661,7 @@ export async function switchBudget(name) {
|
|
|
1644
1661
|
}
|
|
1645
1662
|
connectionPool.updateLoadedSyncId(sessionId, found.syncId);
|
|
1646
1663
|
sessionBudgetState.set(sessionId, key);
|
|
1664
|
+
setPreferredBudgetSyncId(store?.principal, found.syncId); // #189: persist at commit
|
|
1647
1665
|
logger.info(`[ADAPTER] Active budget switched for session ${sessionId} to: "${found.name}" (${found.syncId}) on ${found.serverUrl}`);
|
|
1648
1666
|
return { name: found.name, syncId: found.syncId, serverUrl: found.serverUrl };
|
|
1649
1667
|
}
|
|
@@ -1659,6 +1677,7 @@ export async function switchBudget(name) {
|
|
|
1659
1677
|
// Update the per-session active-budget slot. Subsequent getActiveBudgetConfig
|
|
1660
1678
|
// calls for this session now return the new budget.
|
|
1661
1679
|
sessionBudgetState.set(sessionId, key);
|
|
1680
|
+
setPreferredBudgetSyncId(store?.principal, found.syncId); // #189: persist at commit
|
|
1662
1681
|
// Materialise a fresh pool entry bound to the new budget. Without this, the
|
|
1663
1682
|
// next withActualApi call would find no pool entry and fall back to the
|
|
1664
1683
|
// legacy init+shutdown path. Failure here is logged but not fatal: the
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Per-principal active-budget preference store (#189, Phase 1 of #173).
|
|
2
|
+
//
|
|
3
|
+
// After a server restart the client re-initializes and gets a NEW session, so
|
|
4
|
+
// the per-session active-budget selection (sessionBudgetState in actual-adapter)
|
|
5
|
+
// is lost and the user silently reverts to the env-default budget. This store
|
|
6
|
+
// remembers a principal's last active budget so the new session can restore it.
|
|
7
|
+
//
|
|
8
|
+
// Security model (see #189):
|
|
9
|
+
// * The key at rest is sha256(principal) hex, never the raw OIDC sub / email /
|
|
10
|
+
// token. The file maps hash -> budget syncId only.
|
|
11
|
+
// * It is NEVER trusted as authorization: the caller re-checks the live ACL
|
|
12
|
+
// (allowedBudgets) before applying a restored preference, so a stale
|
|
13
|
+
// preference can never widen access (pickAllowedPreferredBudget below).
|
|
14
|
+
// * It is keyed per principal, not per session-id, so it does not participate
|
|
15
|
+
// in the #167 session-liveness pool and cannot drift with it.
|
|
16
|
+
// * Missing / corrupt / unwritable file degrades to a no-op (the feature
|
|
17
|
+
// simply does nothing); it never throws into the request path.
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync, chmodSync } from 'node:fs';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
|
+
import logger from '../logger.js';
|
|
22
|
+
import config from '../config.js';
|
|
23
|
+
const FILE_NAME = 'budget-preferences.json';
|
|
24
|
+
/** sha256 hex of the principal. Never store the raw principal. */
|
|
25
|
+
function hashPrincipal(principal) {
|
|
26
|
+
return createHash('sha256').update(principal, 'utf8').digest('hex');
|
|
27
|
+
}
|
|
28
|
+
function storePath() {
|
|
29
|
+
const dir = config.MCP_BRIDGE_DATA_DIR;
|
|
30
|
+
if (!dir)
|
|
31
|
+
return null;
|
|
32
|
+
return join(dir, FILE_NAME);
|
|
33
|
+
}
|
|
34
|
+
function readAll() {
|
|
35
|
+
const p = storePath();
|
|
36
|
+
if (!p)
|
|
37
|
+
return {};
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(readFileSync(p, 'utf8'));
|
|
40
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Missing or corrupt file: treat as empty. The feature no-ops rather than
|
|
44
|
+
// crashing the request that triggered the read.
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** The budget syncId this principal last selected, or undefined. */
|
|
49
|
+
export function getPreferredBudgetSyncId(principal) {
|
|
50
|
+
if (!principal)
|
|
51
|
+
return undefined;
|
|
52
|
+
return readAll()[hashPrincipal(principal)];
|
|
53
|
+
}
|
|
54
|
+
/** Persist the principal's active budget. Best-effort: never throws. */
|
|
55
|
+
export function setPreferredBudgetSyncId(principal, syncId) {
|
|
56
|
+
const p = storePath();
|
|
57
|
+
if (!p || !principal || !syncId)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
const all = readAll();
|
|
61
|
+
if (all[hashPrincipal(principal)] === syncId)
|
|
62
|
+
return; // unchanged, skip write
|
|
63
|
+
all[hashPrincipal(principal)] = syncId;
|
|
64
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
65
|
+
// Atomic write: temp + rename so a crash mid-write cannot corrupt the file.
|
|
66
|
+
const tmp = `${p}.tmp`;
|
|
67
|
+
writeFileSync(tmp, JSON.stringify(all), { mode: 0o600 });
|
|
68
|
+
renameSync(tmp, p);
|
|
69
|
+
try {
|
|
70
|
+
chmodSync(p, 0o600);
|
|
71
|
+
}
|
|
72
|
+
catch { /* best-effort perms */ }
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
logger.debug(`[BUDGET-PREF] failed to persist preference (non-fatal): ${e instanceof Error ? e.message : e}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Pure restore decision: given a principal's stored syncId, the live ACL, and
|
|
80
|
+
* the budget registry, return the budget to restore, or undefined.
|
|
81
|
+
*
|
|
82
|
+
* The ACL re-check is the security boundary: a stored preference is applied
|
|
83
|
+
* ONLY if `allowedBudgets` is unrestricted (`['*']`) or contains the budget's
|
|
84
|
+
* syncId. This is why a stale preference can never widen access. Extracted as a
|
|
85
|
+
* pure function so it is unit-testable without the adapter / requestContext.
|
|
86
|
+
*/
|
|
87
|
+
export function pickAllowedPreferredBudget(storedSyncId, allowedBudgets, registryValues) {
|
|
88
|
+
if (!storedSyncId)
|
|
89
|
+
return undefined;
|
|
90
|
+
const found = registryValues.find(b => b.syncId === storedSyncId);
|
|
91
|
+
if (!found)
|
|
92
|
+
return undefined;
|
|
93
|
+
if (allowedBudgets && !allowedBudgets.includes('*') && !allowedBudgets.includes(found.syncId)) {
|
|
94
|
+
return undefined; // ACL no longer permits this budget
|
|
95
|
+
}
|
|
96
|
+
return found;
|
|
97
|
+
}
|
|
@@ -21,6 +21,16 @@ import * as fs from 'node:fs';
|
|
|
21
21
|
// `requestContext` from this module.
|
|
22
22
|
import { requestContext } from '../lib/requestContext.js';
|
|
23
23
|
export { requestContext };
|
|
24
|
+
// Resolve the authenticated principal for the per-principal budget preference
|
|
25
|
+
// (#189). OIDC: the verified JWT subject. Static-bearer: a single shared
|
|
26
|
+
// identity (all bearer callers are the same user). Auth-disabled: undefined, so
|
|
27
|
+
// the preference simply no-ops. Never derived from a client-supplied value.
|
|
28
|
+
function resolvePrincipal(req) {
|
|
29
|
+
if (config.AUTH_PROVIDER === 'oidc') {
|
|
30
|
+
return req.auth?.subject;
|
|
31
|
+
}
|
|
32
|
+
return config.MCP_SSE_AUTHORIZATION ? 'static-bearer' : undefined;
|
|
33
|
+
}
|
|
24
34
|
export async function startHttpServer(mcp, port, httpPath, capabilities, // was passed by index.ts
|
|
25
35
|
implementedTools, // was passed by index.ts
|
|
26
36
|
serverDescription, // was passed by index.ts
|
|
@@ -379,7 +389,7 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
379
389
|
// Run in AsyncLocalStorage context so tools can access sessionId
|
|
380
390
|
// and the adapter can enforce per-request budget ACL (#156).
|
|
381
391
|
const allowedBudgetsInit = req.allowedBudgets;
|
|
382
|
-
await requestContext.run({ sessionId: undefined, allowedBudgets: allowedBudgetsInit }, async () => {
|
|
392
|
+
await requestContext.run({ sessionId: undefined, allowedBudgets: allowedBudgetsInit, principal: resolvePrincipal(req) }, async () => {
|
|
383
393
|
await transport.handleRequest(req, res, req.body);
|
|
384
394
|
});
|
|
385
395
|
}
|
|
@@ -451,14 +461,18 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
451
461
|
});
|
|
452
462
|
return;
|
|
453
463
|
}
|
|
454
|
-
// For other methods, reject the
|
|
464
|
+
// For other methods, reject with the MCP spec signal for a gone
|
|
465
|
+
// session: HTTP 404 + JSON-RPC -32001 "Session not found". This tells a
|
|
466
|
+
// spec-compliant client to start a new session (re-initialize without an
|
|
467
|
+
// mcp-session-id header) instead of treating it as a generic error.
|
|
468
|
+
// Previously this returned a non-spec 400 / -32000 (#188).
|
|
455
469
|
logger.warn(`[SESSION] Session ${sessionId} not found (method: ${method}). Client must re-initialize.`);
|
|
456
|
-
res.status(
|
|
470
|
+
res.status(404).json({
|
|
457
471
|
jsonrpc: '2.0',
|
|
458
472
|
id: payload?.id ?? null,
|
|
459
473
|
error: {
|
|
460
|
-
code: -
|
|
461
|
-
message: 'Session
|
|
474
|
+
code: -32001,
|
|
475
|
+
message: 'Session not found. Please re-initialize by calling initialize without mcp-session-id header.'
|
|
462
476
|
},
|
|
463
477
|
});
|
|
464
478
|
return;
|
|
@@ -469,7 +483,7 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
469
483
|
// Run in AsyncLocalStorage context so tools and the adapter can access
|
|
470
484
|
// sessionId (pool branch, #134) and allowedBudgets (ACL enforcement, #156).
|
|
471
485
|
const allowedBudgets = req.allowedBudgets;
|
|
472
|
-
await requestContext.run({ sessionId, allowedBudgets }, async () => {
|
|
486
|
+
await requestContext.run({ sessionId, allowedBudgets, principal: resolvePrincipal(req) }, async () => {
|
|
473
487
|
await transport.handleRequest(req, res, req.body);
|
|
474
488
|
});
|
|
475
489
|
}
|
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.6.
|
|
4
|
+
"version": "0.6.32",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.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_insecure_upstream.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/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_body_limit.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.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_insecure_upstream.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_body_limit.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",
|
|
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",
|