actual-mcp-server 0.6.31 → 0.6.33
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.33",
|
|
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/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",
|
|
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 && node tests/unit/budget_preference_restore.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
|
}
|
|
@@ -357,6 +369,14 @@ let _skipApiInitForTests = false;
|
|
|
357
369
|
export function _setSkipApiInitForTests(value) {
|
|
358
370
|
_skipApiInitForTests = value;
|
|
359
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Test-only readback of the active budget for the current requestContext, so a
|
|
374
|
+
* restart-replay test (#189) can assert the per-principal preference is restored
|
|
375
|
+
* on a fresh session. NOT part of the package public surface.
|
|
376
|
+
*/
|
|
377
|
+
export function _getActiveBudgetConfigForTests() {
|
|
378
|
+
return getActiveBudgetConfig();
|
|
379
|
+
}
|
|
360
380
|
// ----------------------------------------------------------------------------
|
|
361
381
|
// Auth-rate-limit retry — issue #127
|
|
362
382
|
// ----------------------------------------------------------------------------
|
|
@@ -1607,6 +1627,10 @@ export async function switchBudget(name) {
|
|
|
1607
1627
|
throw new Error(`Budget ACL: budget "${found.name}" (${found.syncId}) is not in this session's allowedBudgets.`);
|
|
1608
1628
|
}
|
|
1609
1629
|
}
|
|
1630
|
+
// #189 Phase 1: the principal's chosen budget is persisted at each commit
|
|
1631
|
+
// point below (paired with the sessionBudgetState write), NOT here, so a
|
|
1632
|
+
// switch that throws before committing never leaves a stale preference. The
|
|
1633
|
+
// helper is keyed by a hash of the principal and never throws.
|
|
1610
1634
|
// Fast path (#172): if the current pool entry's auth descriptor matches the
|
|
1611
1635
|
// target budget's (same serverUrl + password + encryptionPassword), skip
|
|
1612
1636
|
// release + re-auth. Just download the new budget file on the already-
|
|
@@ -1620,6 +1644,7 @@ export async function switchBudget(name) {
|
|
|
1620
1644
|
if (sameAuth && currentEntry.syncId === found.syncId) {
|
|
1621
1645
|
// No-op: already on this exact budget. Keep session map consistent and return.
|
|
1622
1646
|
sessionBudgetState.set(sessionId, key);
|
|
1647
|
+
setPreferredBudgetSyncId(store?.principal, found.syncId); // #189: persist at commit
|
|
1623
1648
|
logger.info(`[ADAPTER] switchBudget no-op for session ${sessionId}: already on "${found.name}" (${found.syncId})`);
|
|
1624
1649
|
return { name: found.name, syncId: found.syncId, serverUrl: found.serverUrl };
|
|
1625
1650
|
}
|
|
@@ -1644,6 +1669,7 @@ export async function switchBudget(name) {
|
|
|
1644
1669
|
}
|
|
1645
1670
|
connectionPool.updateLoadedSyncId(sessionId, found.syncId);
|
|
1646
1671
|
sessionBudgetState.set(sessionId, key);
|
|
1672
|
+
setPreferredBudgetSyncId(store?.principal, found.syncId); // #189: persist at commit
|
|
1647
1673
|
logger.info(`[ADAPTER] Active budget switched for session ${sessionId} to: "${found.name}" (${found.syncId}) on ${found.serverUrl}`);
|
|
1648
1674
|
return { name: found.name, syncId: found.syncId, serverUrl: found.serverUrl };
|
|
1649
1675
|
}
|
|
@@ -1659,6 +1685,7 @@ export async function switchBudget(name) {
|
|
|
1659
1685
|
// Update the per-session active-budget slot. Subsequent getActiveBudgetConfig
|
|
1660
1686
|
// calls for this session now return the new budget.
|
|
1661
1687
|
sessionBudgetState.set(sessionId, key);
|
|
1688
|
+
setPreferredBudgetSyncId(store?.principal, found.syncId); // #189: persist at commit
|
|
1662
1689
|
// Materialise a fresh pool entry bound to the new budget. Without this, the
|
|
1663
1690
|
// next withActualApi call would find no pool entry and fall back to the
|
|
1664
1691
|
// 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
|
}
|
|
@@ -473,7 +483,7 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
473
483
|
// Run in AsyncLocalStorage context so tools and the adapter can access
|
|
474
484
|
// sessionId (pool branch, #134) and allowedBudgets (ACL enforcement, #156).
|
|
475
485
|
const allowedBudgets = req.allowedBudgets;
|
|
476
|
-
await requestContext.run({ sessionId, allowedBudgets }, async () => {
|
|
486
|
+
await requestContext.run({ sessionId, allowedBudgets, principal: resolvePrincipal(req) }, async () => {
|
|
477
487
|
await transport.handleRequest(req, res, req.body);
|
|
478
488
|
});
|
|
479
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.33",
|
|
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/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",
|
|
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 && node tests/unit/budget_preference_restore.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",
|