@bevel-software/platform-core-backend 0.3.1 → 0.3.2
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/dist/modules/mcp/manual-failure-memo.d.ts +16 -1
- package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
- package/dist/modules/mcp/manual-failure-memo.js +23 -1
- package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +6 -2
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js +5 -0
- package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
- package/package.json +2 -2
- package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +70 -54
- package/src/modules/mcp/manual-failure-memo.ts +89 -67
- package/src/modules/mcp/mcp.service.ts +1011 -1007
- package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +39 -0
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +5 -0
|
@@ -1,67 +1,89 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Short-lived memo of manual-registration FAILURES, keyed per (user, manual).
|
|
3
|
-
*
|
|
4
|
-
* Registering a `.tool` manual with a present-but-broken credential (an
|
|
5
|
-
* expired OAuth session, a revoked key) fails with a live network round-trip —
|
|
6
|
-
* and sessions are rebuilt constantly (every external MCP connection runs a
|
|
7
|
-
* full discovery), so one stale credential otherwise re-dials its provider on
|
|
8
|
-
* every build, forever. Besides the log noise, each failed attempt on the
|
|
9
|
-
* MCP transport layer's long-lived shared session leaks an abort listener
|
|
10
|
-
* upstream — the memo starves that loop.
|
|
11
|
-
*
|
|
12
|
-
* Failures are remembered for a few minutes only: a repaired credential is
|
|
13
|
-
* picked up at the next build after expiry (or immediately after a restart).
|
|
14
|
-
* Successes are never cached here — this is a circuit breaker, not a catalog
|
|
15
|
-
* cache. Expired entries are pruned opportunistically so the map stays
|
|
16
|
-
* bounded by the number of DISTINCT recently-failing (user, manual) pairs.
|
|
17
|
-
*/
|
|
18
|
-
export class ManualFailureMemo {
|
|
19
|
-
private readonly failures = new Map<string, { message: string; expiresAt: number }>();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return this.
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Short-lived memo of manual-registration FAILURES, keyed per (user, manual).
|
|
3
|
+
*
|
|
4
|
+
* Registering a `.tool` manual with a present-but-broken credential (an
|
|
5
|
+
* expired OAuth session, a revoked key) fails with a live network round-trip —
|
|
6
|
+
* and sessions are rebuilt constantly (every external MCP connection runs a
|
|
7
|
+
* full discovery), so one stale credential otherwise re-dials its provider on
|
|
8
|
+
* every build, forever. Besides the log noise, each failed attempt on the
|
|
9
|
+
* MCP transport layer's long-lived shared session leaks an abort listener
|
|
10
|
+
* upstream — the memo starves that loop.
|
|
11
|
+
*
|
|
12
|
+
* Failures are remembered for a few minutes only: a repaired credential is
|
|
13
|
+
* picked up at the next build after expiry (or immediately after a restart).
|
|
14
|
+
* Successes are never cached here — this is a circuit breaker, not a catalog
|
|
15
|
+
* cache. Expired entries are pruned opportunistically so the map stays
|
|
16
|
+
* bounded by the number of DISTINCT recently-failing (user, manual) pairs.
|
|
17
|
+
*/
|
|
18
|
+
export class ManualFailureMemo {
|
|
19
|
+
private readonly failures = new Map<string, { message: string; expiresAt: number }>();
|
|
20
|
+
/**
|
|
21
|
+
* Advanced by every clear operation. A registration attempt captures the
|
|
22
|
+
* generation BEFORE its (slow, awaited) network call and hands it back to
|
|
23
|
+
* {@link recordFailure} — if a clear happened in between (the user just
|
|
24
|
+
* repaired the credential), the stale in-flight failure is discarded instead
|
|
25
|
+
* of resurrecting a memo entry the clear was meant to remove.
|
|
26
|
+
*/
|
|
27
|
+
private generation = 0;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly ttlMs: number = 5 * 60_000,
|
|
31
|
+
private readonly now: () => number = Date.now,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
/** Capture before an awaited registration attempt; pass to {@link recordFailure}. */
|
|
35
|
+
get currentGeneration(): number {
|
|
36
|
+
return this.generation;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private key(userId: string, manualName: string): string {
|
|
40
|
+
return `${userId} ${manualName}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The remembered failure message when (user, manual) failed recently; undefined otherwise. */
|
|
44
|
+
recentFailure(userId: string, manualName: string): string | undefined {
|
|
45
|
+
const now = this.now();
|
|
46
|
+
for (const [k, entry] of this.failures) {
|
|
47
|
+
if (entry.expiresAt <= now) this.failures.delete(k);
|
|
48
|
+
}
|
|
49
|
+
return this.failures.get(this.key(userId, manualName))?.message;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Record a failure — unless `generation` (captured before the attempt) is
|
|
54
|
+
* stale, meaning a clear ran while the attempt was in flight. Dropping the
|
|
55
|
+
* record is the conservative direction: the worst case is one extra retry.
|
|
56
|
+
*/
|
|
57
|
+
recordFailure(userId: string, manualName: string, message: string, generation?: number): void {
|
|
58
|
+
if (generation !== undefined && generation !== this.generation) return;
|
|
59
|
+
this.failures.set(this.key(userId, manualName), {
|
|
60
|
+
message,
|
|
61
|
+
expiresAt: this.now() + this.ttlMs,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Forget a pair (e.g. after a successful registration proves the credential works again). */
|
|
66
|
+
clear(userId: string, manualName: string): void {
|
|
67
|
+
this.generation += 1;
|
|
68
|
+
this.failures.delete(this.key(userId, manualName));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Forget every failure for one user — called when their secrets change, so a
|
|
73
|
+
* just-repaired credential is retried on the VERY NEXT build instead of
|
|
74
|
+
* waiting out the TTL.
|
|
75
|
+
*/
|
|
76
|
+
clearUser(userId: string): void {
|
|
77
|
+
this.generation += 1;
|
|
78
|
+
const prefix = `${userId} `;
|
|
79
|
+
for (const k of this.failures.keys()) {
|
|
80
|
+
if (k.startsWith(prefix)) this.failures.delete(k);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Forget everything — a SHARED secret changed, which can affect any user. */
|
|
85
|
+
clearAll(): void {
|
|
86
|
+
this.generation += 1;
|
|
87
|
+
this.failures.clear();
|
|
88
|
+
}
|
|
89
|
+
}
|