@sentry/junior 0.257.0 → 0.259.0
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/{actor-BTtLaeWy.js → actor-tlxzqaRe.js} +3 -6
- package/dist/adapter-Bn0gFVSF.js +270 -0
- package/dist/{admin-DgIkm6Vy.js → admin-C8xz2f10.js} +2 -3
- package/dist/{agent-hooks-BjtTzvru.js → agent-hooks-B00FaKdP.js} +70 -109
- package/dist/{agent-invocations-Bdu3ROfo.js → agent-invocations-Qfz7dWgU.js} +6 -12
- package/dist/{agent-runner-D7OboVb3.js → agent-runner-CPSd7rKm.js} +324 -742
- package/dist/api/schema.d.ts +442 -447
- package/dist/api/schema.js +6 -6
- package/dist/api.d.ts +2 -12
- package/dist/api.js +165 -158
- package/dist/{app-BsU5QNBd.d.ts → app-Bbxrehjf.d.ts} +25 -45
- package/dist/app.d.ts +3 -3
- package/dist/app.js +206 -321
- package/dist/{assistant-reply-CbTPebzL.js → assistant-reply-CqthTerd.js} +3 -5
- package/dist/{automation-CHdlHVZP.js → automation-DWBFBenG.js} +6 -11
- package/dist/{briefs-oLOldnDM.js → briefs-DNYbQ9cD.js} +8 -11
- package/dist/{cadence-D00SCzT1.js → cadence-R7Em4RjE.js} +1 -2
- package/dist/{catalog-runtime-CZzhGQ7F.js → catalog-runtime-B8SuFPGb.js} +3 -4
- package/dist/cli/chat.d.ts +2 -4
- package/dist/cli/chat.js +22 -24
- package/dist/cli/check.d.ts +1 -3
- package/dist/cli/check.js +4 -5
- package/dist/cli/env.d.ts +1 -3
- package/dist/cli/env.js +1 -2
- package/dist/cli/init.d.ts +1 -3
- package/dist/cli/init.js +3 -3
- package/dist/cli/main.d.ts +1 -3
- package/dist/cli/main.js +8 -5
- package/dist/cli/plugins.d.ts +2 -4
- package/dist/cli/plugins.js +7 -8
- package/dist/cli/run.d.ts +3 -3
- package/dist/cli/run.js +3 -3
- package/dist/cli/snapshot-create.d.ts +1 -3
- package/dist/cli/snapshot-create.js +5 -5
- package/dist/cli/upgrade.d.ts +3 -6
- package/dist/cli/upgrade.js +11 -17
- package/dist/{client-D8HnkOQX.js → client-BZdxgKyL.js} +64 -18
- package/dist/{config-CJRnWK9H.js → config-Deqpfpg9.js} +21 -15
- package/dist/{connections-ClYrKzpX.js → connections-9gJkfS85.js} +179 -20
- package/dist/{conversation-CKNLYBJt.js → conversation-DjN3mlog.js} +8 -15
- package/dist/{conversation-privacy-C8b1nphZ.js → conversation-privacy-CFhNKxp-.js} +2 -3
- package/dist/{conversation-work-C6y3T5Hx.js → conversation-work-BaT_T6EB.js} +1 -2
- package/dist/{credential-sync-BXfohNmJ.js → credential-sync-BABGymK3.js} +23 -190
- package/dist/{db-Ca9Nt5ci.d.ts → db-BnRqQ7G5.d.ts} +1 -1
- package/dist/{executor-Do0THDRR.js → executor-Bb3eQb8q.js} +30 -52
- package/dist/{experimental-CQIvMUO7.js → experimental-BNK1l1jv.js} +2 -4
- package/dist/{history-9pO1Zd4Y.js → history-AY8rs-FR.js} +5 -9
- package/dist/{history-DvqWZlYX.d.ts → history-BgI33Kie.d.ts} +12 -20
- package/dist/{images-BN_BiBXF.js → images-BbSdVHC6.js} +5 -6
- package/dist/instrumentation.d.ts +1 -3
- package/dist/instrumentation.js +6 -8
- package/dist/{job-queue-B6SV9uVa.js → job-queue-vdGU-5ge.js} +6 -12
- package/dist/{logging-CWxiIuCF.js → logging-BStXN56b.js} +7 -12
- package/dist/{message-TUOdbSBW.js → message-DRf6m4uO.js} +5 -6
- package/dist/{model-profile-BwU8mqwD.js → model-profile-Bg8EoCKD.js} +2 -4
- package/dist/nitro.d.ts +3 -5
- package/dist/nitro.js +12 -17
- package/dist/{oauth-authorization-BA4Dt6a7.js → oauth-authorization-djQn5Nhy.js} +2 -4
- package/dist/{oauth-callback-server-Dj5UJODo.js → oauth-callback-server-JmOgLcGl.js} +3 -4
- package/dist/{oauth-relay-CdCnu80W.js → oauth-relay-CTygMkNx.js} +2 -3
- package/dist/{package-discovery-BVvSnQBB.js → package-discovery-DJBompdd.js} +17 -21
- package/dist/{pending-auth-s_QFribU.js → pending-auth-rG5U_QS1.js} +92 -83
- package/dist/{plugin-module-CVvEUSMH.js → plugin-module-Cf5gVPWP.js} +1 -2
- package/dist/{plugins-CjfUP5gJ.d.ts → plugins-B_0Cr0MO.d.ts} +5 -5
- package/dist/{plugins-egKWjgue.js → plugins-DOm9Y6qI.js} +2 -3
- package/dist/{private-trace-result-BLvBjy_e.js → private-trace-result-CE7_lEdq.js} +1 -2
- package/dist/{profile-h-72wM78.js → profile-qBM6B_-V.js} +14 -295
- package/dist/{prompt-DyoEj9c2.js → prompt-xgpyMtg7.js} +5 -9
- package/dist/{queue-B5SnqRln.d.ts → queue-C37RbDVc.d.ts} +0 -2
- package/dist/{registry-sLTr-sV9.js → registry-B_M_x_iS.js} +56 -30
- package/dist/{resolve-ncQPZROQ.js → resolve-BzpuLh8c.js} +8 -12
- package/dist/{result-C883vngT.js → result-DEZH_AnZ.js} +13 -17
- package/dist/{rolldown-runtime-B4FMCO8f.js → rolldown-runtime-nwUcYXjP.js} +1 -2
- package/dist/{runner-CzYImdOF.js → runner-BqQlG10t.js} +12 -13
- package/dist/{sandbox-egress-signals-CZbMINHI.js → sandbox-egress-signals-DCq3WLs5.js} +2 -3
- package/dist/{schema-Dy69D4Ge.js → schema-CF9HqNRY.js} +30 -27
- package/dist/{sentry-CgWZM_kV.js → sentry-BBUlHih9.js} +2 -3
- package/dist/service-account-BqTBkyD7.js +120 -0
- package/dist/service-account-BzhW7kXu.js +661 -0
- package/dist/{session-stop-CQjKd4a2.js → session-stop-CV65ZlU3.js} +23 -33
- package/dist/{skills-C22SLruK.js → skills-CiWS5_Ub.js} +9 -9
- package/dist/{spawn-DY9CLXDL.js → spawn-BSAQ7UQZ.js} +4 -5
- package/dist/{store-DXFIQy-G.js → store-B1GES4gC.js} +13 -15
- package/dist/{store-DoewL5mG.d.ts → store-CG0VJyye.d.ts} +0 -14
- package/dist/{task-queue-B17vV0La.js → task-queue-BAeGEMjJ.js} +5 -7
- package/dist/{task-runner-DUjvzYXj.js → task-runner-BlGhcQPi.js} +22 -26
- package/dist/{tasks-D_WIJtvO.js → tasks-DieKX3d4.js} +63 -78
- package/dist/{thread-state-Ch4yuCqG.js → thread-state-DpSWS4Rr.js} +13 -19
- package/dist/{turn-failure-response-C5nBKC4S.js → turn-failure-response-DQljogGJ.js} +16 -22
- package/dist/{turn-lifecycle-CO-ylUmV.js → turn-lifecycle-BLgrkV_L.js} +2 -3
- package/dist/{turn-session-routing-B3LWooK5.js → turn-session-routing-BlajheOU.js} +2 -3
- package/dist/{validation-CXYxaNYx.js → validation-23d7K9tX.js} +3 -4
- package/dist/vercel.d.ts +1 -3
- package/dist/vercel.js +1 -2
- package/dist/{version-BcdZy2zg.js → version-B_XumRx-.js} +3 -5
- package/dist/{version-CnXZ614E.d.ts → version-Cc2OFd9G.d.ts} +0 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -2
- package/dist/{viewer-DSZXygMU.js → viewer-B2_ABsYi.js} +26 -57
- package/dist/{web-input-B9Bv0_lR.js → web-input-BZDyxpzR.js} +101 -158
- package/dist/{web-message-id-xPI_EFSC.js → web-message-id-CJSUiksE.js} +3 -6
- package/dist/{work-nAfDSUFP.js → work-D3QKiV0O.js} +14 -16
- package/dist/{worker-vGXZ6XFT.js → worker-C8mUjYf9.js} +7 -8
- package/package.json +10 -9
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import { m as logWarn, y as setSpanAttributes, z as isRecord } from "./logging-BStXN56b.js";
|
|
2
|
+
import { c as fetchWithBoundedOAuthErrorBodies, i as resolvePluginHeaderEnvRefs } from "./registry-B_M_x_iS.js";
|
|
3
|
+
import { i as MUTATION_LOCK_TTL_MS, o as fenceLock, r as getStateAdapter, s as withLock } from "./adapter-Bn0gFVSF.js";
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { UnauthorizedError, auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
7
|
+
import { Client } from "@modelcontextprotocol/sdk/client";
|
|
8
|
+
import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
9
|
+
// src/chat/mcp/errors.ts
|
|
10
|
+
/** Safe failure at an MCP network or OAuth boundary. */
|
|
11
|
+
var McpProviderError = class extends Error {
|
|
12
|
+
phase;
|
|
13
|
+
provider;
|
|
14
|
+
missingSession;
|
|
15
|
+
resourceHost;
|
|
16
|
+
status;
|
|
17
|
+
constructor(details) {
|
|
18
|
+
super(`MCP provider ${details.phase.replace("_", " ")} failed`);
|
|
19
|
+
this.name = "McpProviderError";
|
|
20
|
+
this.phase = details.phase;
|
|
21
|
+
this.provider = details.provider;
|
|
22
|
+
this.missingSession = details.missingSession;
|
|
23
|
+
this.resourceHost = details.resourceHost;
|
|
24
|
+
this.status = details.status;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function providerStatus(error) {
|
|
28
|
+
if (!error || typeof error !== "object") return;
|
|
29
|
+
const value = error;
|
|
30
|
+
const status = typeof value.status === "number" ? value.status : typeof value.code === "number" ? value.code : void 0;
|
|
31
|
+
return status !== void 0 && status >= 100 && status <= 599 ? status : void 0;
|
|
32
|
+
}
|
|
33
|
+
/** Replace an external MCP failure without retaining its provider-controlled cause. */
|
|
34
|
+
function toMcpProviderError(error, details) {
|
|
35
|
+
if (error instanceof McpProviderError) return error;
|
|
36
|
+
return new McpProviderError({
|
|
37
|
+
...details,
|
|
38
|
+
status: details.status ?? providerStatus(error)
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Return safe MCP provider fields for logs and spans. */
|
|
42
|
+
function getMcpProviderErrorAttributes(error) {
|
|
43
|
+
if (!(error instanceof McpProviderError)) return {};
|
|
44
|
+
return {
|
|
45
|
+
"app.credential.provider": error.provider,
|
|
46
|
+
"app.mcp.error.phase": error.phase,
|
|
47
|
+
...error.status !== void 0 ? { "http.response.status_code": error.status } : void 0,
|
|
48
|
+
...error.resourceHost ? { "server.address": error.resourceHost } : void 0
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Thrown when an MCP failure should be returned as a model-visible tool error. */
|
|
52
|
+
var McpToolError = class extends Error {
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "McpToolError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Thrown when a plugin uses only its service account and no Admin has
|
|
60
|
+
* connected it. The model can tell the person to ask an Admin.
|
|
61
|
+
*/
|
|
62
|
+
var McpServiceAccountRequiredError = class extends McpToolError {
|
|
63
|
+
provider;
|
|
64
|
+
constructor(provider, displayName) {
|
|
65
|
+
super(`${displayName} is not available yet. Junior uses a shared ${displayName} account for everyone, and a Junior admin must connect it on the dashboard Admin page.`);
|
|
66
|
+
this.name = "McpServiceAccountRequiredError";
|
|
67
|
+
this.provider = provider;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/** Return the OpenTelemetry error.type value for MCP-aware tool failures. */
|
|
71
|
+
function getMcpAwareErrorType(error, fallback) {
|
|
72
|
+
if (error instanceof McpToolError) return "tool_error";
|
|
73
|
+
if (error instanceof McpProviderError) return "mcp_provider_error";
|
|
74
|
+
return error instanceof Error ? error.name : fallback;
|
|
75
|
+
}
|
|
76
|
+
/** Return the display-safe error message for MCP-aware tool failures. */
|
|
77
|
+
function getMcpAwareErrorMessage(error) {
|
|
78
|
+
return error instanceof Error ? error.message : String(error);
|
|
79
|
+
}
|
|
80
|
+
/** Return an error message safe for logs and span attributes. */
|
|
81
|
+
function getMcpAwareTelemetryMessage(error, _privacy) {
|
|
82
|
+
if (error instanceof McpToolError) return "MCP tool call failed";
|
|
83
|
+
if (error instanceof McpProviderError) return error.message;
|
|
84
|
+
return getMcpAwareErrorMessage(error);
|
|
85
|
+
}
|
|
86
|
+
// src/chat/mcp/client.ts
|
|
87
|
+
const MCP_CLIENT_INFO = {
|
|
88
|
+
name: "junior-mcp-client",
|
|
89
|
+
version: "1.0.0"
|
|
90
|
+
};
|
|
91
|
+
const MCP_TOOLS_CALL_METHOD = "tools/call";
|
|
92
|
+
function getDefaultPort(url) {
|
|
93
|
+
if (url.protocol === "https:") return 443;
|
|
94
|
+
if (url.protocol === "http:") return 80;
|
|
95
|
+
}
|
|
96
|
+
function getMcpNetworkAttributes(url) {
|
|
97
|
+
const port = url.port ? Number(url.port) : getDefaultPort(url);
|
|
98
|
+
return {
|
|
99
|
+
"server.address": url.hostname,
|
|
100
|
+
...port !== void 0 ? { "server.port": port } : void 0,
|
|
101
|
+
...url.protocol === "http:" || url.protocol === "https:" ? {
|
|
102
|
+
"network.protocol.name": "http",
|
|
103
|
+
"network.transport": "tcp"
|
|
104
|
+
} : void 0
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
var McpAuthorizationRequiredError = class extends Error {
|
|
108
|
+
provider;
|
|
109
|
+
constructor(provider, message) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.name = "McpAuthorizationRequiredError";
|
|
112
|
+
this.provider = provider;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function isMissingSessionError(error) {
|
|
116
|
+
return error instanceof StreamableHTTPError && (error.code === 404 || /Session not found/i.test(error.message));
|
|
117
|
+
}
|
|
118
|
+
var PluginMcpClient = class {
|
|
119
|
+
client;
|
|
120
|
+
clientPromise;
|
|
121
|
+
lastAttemptedTransportSessionId;
|
|
122
|
+
providerStatusStore = new AsyncLocalStorage();
|
|
123
|
+
transport;
|
|
124
|
+
listedTools;
|
|
125
|
+
resourceHost;
|
|
126
|
+
plugin;
|
|
127
|
+
options;
|
|
128
|
+
constructor(plugin, options = {}) {
|
|
129
|
+
this.plugin = plugin;
|
|
130
|
+
this.options = options;
|
|
131
|
+
const url = plugin.manifest.mcp?.url;
|
|
132
|
+
this.resourceHost = url ? new URL(url).hostname : void 0;
|
|
133
|
+
}
|
|
134
|
+
async listTools() {
|
|
135
|
+
if (this.listedTools) return [...this.listedTools];
|
|
136
|
+
return await this.withSessionRecovery(async () => {
|
|
137
|
+
const client = await this.getClient();
|
|
138
|
+
const discovered = [];
|
|
139
|
+
const seen = /* @__PURE__ */ new Set();
|
|
140
|
+
let cursor;
|
|
141
|
+
do {
|
|
142
|
+
const result = await this.wrapAuth(() => client.listTools(cursor ? { cursor } : void 0), "list_tools");
|
|
143
|
+
await this.syncTransportSessionId();
|
|
144
|
+
for (const tool of result.tools) {
|
|
145
|
+
if (seen.has(tool.name)) continue;
|
|
146
|
+
seen.add(tool.name);
|
|
147
|
+
discovered.push(tool);
|
|
148
|
+
}
|
|
149
|
+
cursor = result.nextCursor;
|
|
150
|
+
} while (cursor);
|
|
151
|
+
this.listedTools = discovered.sort((left, right) => left.name.localeCompare(right.name));
|
|
152
|
+
return [...this.listedTools];
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async callTool(name, args, signal) {
|
|
156
|
+
return await this.withSessionRecovery(async () => {
|
|
157
|
+
const client = await this.getClient();
|
|
158
|
+
const mcp = this.plugin.manifest.mcp;
|
|
159
|
+
if (mcp) {
|
|
160
|
+
const url = new URL(mcp.url);
|
|
161
|
+
setSpanAttributes({
|
|
162
|
+
"mcp.method.name": MCP_TOOLS_CALL_METHOD,
|
|
163
|
+
"gen_ai.operation.name": "execute_tool",
|
|
164
|
+
...this.transport?.sessionId ? { "mcp.session.id": this.transport.sessionId } : void 0,
|
|
165
|
+
...this.transport?.protocolVersion ? { "mcp.protocol.version": this.transport.protocolVersion } : void 0,
|
|
166
|
+
...getMcpNetworkAttributes(url)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const result = await this.wrapAuth(() => client.callTool({
|
|
170
|
+
name,
|
|
171
|
+
arguments: args ?? {}
|
|
172
|
+
}, void 0, signal ? { signal } : void 0), "call_tool");
|
|
173
|
+
await this.syncTransportSessionId();
|
|
174
|
+
return result;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async close() {
|
|
178
|
+
await this.disposeClient();
|
|
179
|
+
}
|
|
180
|
+
async withSessionRecovery(operation) {
|
|
181
|
+
try {
|
|
182
|
+
return await operation();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
await this.syncTransportSessionId();
|
|
185
|
+
if (!await this.shouldResetMissingSession(error)) throw error;
|
|
186
|
+
await this.clearStoredTransportSessionId();
|
|
187
|
+
await this.cleanupClientAfterFailure();
|
|
188
|
+
return await operation();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async getClient() {
|
|
192
|
+
if (this.client) return this.client;
|
|
193
|
+
if (this.clientPromise) return await this.clientPromise;
|
|
194
|
+
const clientPromise = this.connectClient();
|
|
195
|
+
this.clientPromise = clientPromise;
|
|
196
|
+
try {
|
|
197
|
+
return await clientPromise;
|
|
198
|
+
} finally {
|
|
199
|
+
if (this.clientPromise === clientPromise) this.clientPromise = void 0;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async connectClient() {
|
|
203
|
+
const mcp = this.plugin.manifest.mcp;
|
|
204
|
+
if (!mcp) throw new Error(`Plugin "${this.plugin.manifest.name}" does not declare MCP config`);
|
|
205
|
+
const headers = mcp.headers ? resolvePluginHeaderEnvRefs(this.plugin.manifest.name, mcp.headers, "MCP header") : void 0;
|
|
206
|
+
const sessionId = await this.getStoredTransportSessionId();
|
|
207
|
+
this.lastAttemptedTransportSessionId = sessionId;
|
|
208
|
+
const transport = new StreamableHTTPClientTransport(new URL(mcp.url), {
|
|
209
|
+
...headers ? { requestInit: { headers } } : void 0,
|
|
210
|
+
fetch: fetchWithBoundedOAuthErrorBodies(this.options.fetch, (status) => {
|
|
211
|
+
const store = this.providerStatusStore.getStore();
|
|
212
|
+
if (store) store.status = status;
|
|
213
|
+
}),
|
|
214
|
+
...this.options.authProvider ? { authProvider: this.options.authProvider } : void 0,
|
|
215
|
+
...sessionId ? { sessionId } : void 0
|
|
216
|
+
});
|
|
217
|
+
const client = new Client(MCP_CLIENT_INFO, { capabilities: {} });
|
|
218
|
+
this.transport = transport;
|
|
219
|
+
try {
|
|
220
|
+
await this.wrapAuth(() => client.connect(transport), "connect");
|
|
221
|
+
this.client = client;
|
|
222
|
+
await this.syncTransportSessionId();
|
|
223
|
+
return client;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
await this.syncTransportSessionId();
|
|
226
|
+
await this.cleanupClientAfterFailure();
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async wrapAuth(operation, phase) {
|
|
231
|
+
const store = {};
|
|
232
|
+
return await this.providerStatusStore.run(store, async () => {
|
|
233
|
+
try {
|
|
234
|
+
return await operation();
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error instanceof McpAuthorizationRequiredError) throw error;
|
|
237
|
+
if (error instanceof UnauthorizedError) throw new McpAuthorizationRequiredError(this.plugin.manifest.name, `MCP authorization required for plugin "${this.plugin.manifest.name}"`);
|
|
238
|
+
throw toMcpProviderError(error, {
|
|
239
|
+
phase,
|
|
240
|
+
provider: this.plugin.manifest.name,
|
|
241
|
+
missingSession: isMissingSessionError(error) || void 0,
|
|
242
|
+
resourceHost: this.resourceHost,
|
|
243
|
+
status: store.status
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
async shouldResetMissingSession(error) {
|
|
249
|
+
if (!(error instanceof McpProviderError && error.missingSession || isMissingSessionError(error))) return false;
|
|
250
|
+
return Boolean(this.transport?.sessionId ?? this.lastAttemptedTransportSessionId ?? await this.getStoredTransportSessionId());
|
|
251
|
+
}
|
|
252
|
+
resetClientState() {
|
|
253
|
+
const transport = this.transport;
|
|
254
|
+
this.listedTools = void 0;
|
|
255
|
+
this.transport = void 0;
|
|
256
|
+
this.client = void 0;
|
|
257
|
+
this.clientPromise = void 0;
|
|
258
|
+
return transport;
|
|
259
|
+
}
|
|
260
|
+
async cleanupClientAfterFailure() {
|
|
261
|
+
const transport = this.resetClientState();
|
|
262
|
+
if (!transport) return;
|
|
263
|
+
try {
|
|
264
|
+
await transport.close();
|
|
265
|
+
} catch {}
|
|
266
|
+
}
|
|
267
|
+
async disposeClient() {
|
|
268
|
+
const transport = this.resetClientState();
|
|
269
|
+
if (transport) try {
|
|
270
|
+
await transport.close();
|
|
271
|
+
} catch (error) {
|
|
272
|
+
throw toMcpProviderError(error, {
|
|
273
|
+
phase: "close",
|
|
274
|
+
provider: this.plugin.manifest.name,
|
|
275
|
+
resourceHost: this.resourceHost
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async getStoredTransportSessionId() {
|
|
280
|
+
return await this.options.authProvider?.getMcpServerSessionId?.();
|
|
281
|
+
}
|
|
282
|
+
async clearStoredTransportSessionId() {
|
|
283
|
+
const provider = this.options.authProvider;
|
|
284
|
+
this.lastAttemptedTransportSessionId = void 0;
|
|
285
|
+
await provider?.saveMcpServerSessionId?.(void 0);
|
|
286
|
+
}
|
|
287
|
+
async syncTransportSessionId() {
|
|
288
|
+
const provider = this.options.authProvider;
|
|
289
|
+
const sessionId = this.transport?.sessionId;
|
|
290
|
+
if (!provider?.saveMcpServerSessionId || !sessionId) return;
|
|
291
|
+
this.lastAttemptedTransportSessionId = sessionId;
|
|
292
|
+
await provider.saveMcpServerSessionId(sessionId);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
// src/chat/mcp/service-account.ts
|
|
296
|
+
/**
|
|
297
|
+
* MCP service account credentials.
|
|
298
|
+
*
|
|
299
|
+
* This is the MCP surface of a plugin's service account. See
|
|
300
|
+
* `@/chat/credentials/service-account` for the credential order and the
|
|
301
|
+
* policy. An Admin connects the account from the dashboard Admin page, or
|
|
302
|
+
* with `junior service-account connect <plugin>`. The credential stays on
|
|
303
|
+
* the host. The sandbox and the model never get it.
|
|
304
|
+
*/
|
|
305
|
+
const SERVICE_ACCOUNT_KEY_PREFIX = "junior:mcp_service_account";
|
|
306
|
+
const SIGN_IN_KEY_PREFIX = "junior:mcp_service_account_sign_in";
|
|
307
|
+
const SIGN_IN_TTL_MS = 6e5;
|
|
308
|
+
const UPDATE_LOCK_WAIT_MS = 5e3;
|
|
309
|
+
function serviceAccountKey(provider) {
|
|
310
|
+
return `${SERVICE_ACCOUNT_KEY_PREFIX}:${provider}`;
|
|
311
|
+
}
|
|
312
|
+
function parseCredentials(value) {
|
|
313
|
+
if (typeof value !== "string") return;
|
|
314
|
+
try {
|
|
315
|
+
const parsed = JSON.parse(value);
|
|
316
|
+
if (!isRecord(parsed) || !isRecord(parsed.clientInformation) || !isRecord(parsed.tokens) || typeof parsed.connectedAtMs !== "number" || typeof parsed.redirectUrl !== "string") return;
|
|
317
|
+
return {
|
|
318
|
+
clientInformation: parsed.clientInformation,
|
|
319
|
+
connectedAtMs: parsed.connectedAtMs,
|
|
320
|
+
...typeof parsed.connectedBy === "string" ? { connectedBy: parsed.connectedBy } : void 0,
|
|
321
|
+
...isRecord(parsed.discoveryState) ? { discoveryState: parsed.discoveryState } : void 0,
|
|
322
|
+
redirectUrl: parsed.redirectUrl,
|
|
323
|
+
tokens: parsed.tokens
|
|
324
|
+
};
|
|
325
|
+
} catch {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/** Read the connected service account for one plugin, if any. */
|
|
330
|
+
async function getMcpServiceAccountCredentials(provider) {
|
|
331
|
+
return parseCredentials(await getStateAdapter().get(serviceAccountKey(provider)));
|
|
332
|
+
}
|
|
333
|
+
/** Save the service account for one plugin. It does not expire. */
|
|
334
|
+
async function putMcpServiceAccountCredentials(provider, credentials) {
|
|
335
|
+
await getStateAdapter().set(serviceAccountKey(provider), JSON.stringify(credentials));
|
|
336
|
+
}
|
|
337
|
+
/** Remove the service account for one plugin. The provider grant stays until the provider revokes it. */
|
|
338
|
+
async function deleteMcpServiceAccountCredentials(provider) {
|
|
339
|
+
await getStateAdapter().delete(serviceAccountKey(provider));
|
|
340
|
+
}
|
|
341
|
+
function clientMetadata(redirectUrl) {
|
|
342
|
+
return {
|
|
343
|
+
client_name: "Junior MCP Service Account",
|
|
344
|
+
redirect_uris: [redirectUrl],
|
|
345
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
346
|
+
response_types: ["code"],
|
|
347
|
+
token_endpoint_auth_method: "none"
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function reconnectMessage(provider) {
|
|
351
|
+
return `The ${provider} service account is not authorized anymore. A Junior admin must run \`junior service-account connect ${provider}\`.`;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Build the OAuth client provider for a connected service account.
|
|
355
|
+
*
|
|
356
|
+
* It refreshes tokens like a user connection but never starts a new sign-in.
|
|
357
|
+
* Many turns share this account, so it reads the store on each request and
|
|
358
|
+
* writes each change under a lock. When the provider rejects a refresh token
|
|
359
|
+
* that another turn already replaced, it retries with the stored one. It
|
|
360
|
+
* deletes the stored grant only when the provider rejected the stored token
|
|
361
|
+
* itself, and then throws `McpAuthorizationRequiredError` so the tool manager
|
|
362
|
+
* can fall back to Actor auth or report the missing account. A refresh that
|
|
363
|
+
* fails for another reason, such as a network error, keeps the account.
|
|
364
|
+
*/
|
|
365
|
+
function createMcpServiceAccountClientProvider(provider, connected) {
|
|
366
|
+
let lastRefreshToken = connected.tokens.refresh_token;
|
|
367
|
+
let tokensRejected = false;
|
|
368
|
+
const withStoreLock = async (run) => {
|
|
369
|
+
const state = getStateAdapter();
|
|
370
|
+
const result = await withLock(state, `${serviceAccountKey(provider)}:update`, async (lock) => await run(() => fenceLock(state, lock)), {
|
|
371
|
+
ttlMs: MUTATION_LOCK_TTL_MS,
|
|
372
|
+
waitMs: UPDATE_LOCK_WAIT_MS
|
|
373
|
+
});
|
|
374
|
+
if (!result.acquired) throw new Error(`Timed out waiting to update the ${provider} service account`);
|
|
375
|
+
return result.value;
|
|
376
|
+
};
|
|
377
|
+
const update = async (patch) => {
|
|
378
|
+
await withStoreLock(async (fence) => {
|
|
379
|
+
const current = await getMcpServiceAccountCredentials(provider);
|
|
380
|
+
if (!current) throw new Error(reconnectMessage(provider));
|
|
381
|
+
await fence();
|
|
382
|
+
await putMcpServiceAccountCredentials(provider, {
|
|
383
|
+
...current,
|
|
384
|
+
...patch
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
redirectUrl: connected.redirectUrl,
|
|
390
|
+
clientMetadata: clientMetadata(connected.redirectUrl),
|
|
391
|
+
clientInformation: async () => (await getMcpServiceAccountCredentials(provider))?.clientInformation,
|
|
392
|
+
saveClientInformation: async (clientInformation) => {
|
|
393
|
+
await update({ clientInformation });
|
|
394
|
+
},
|
|
395
|
+
tokens: async () => {
|
|
396
|
+
const tokens = (await getMcpServiceAccountCredentials(provider))?.tokens;
|
|
397
|
+
if (tokensRejected) {
|
|
398
|
+
if (!tokens || tokens.refresh_token === lastRefreshToken) return;
|
|
399
|
+
tokensRejected = false;
|
|
400
|
+
}
|
|
401
|
+
lastRefreshToken = tokens?.refresh_token;
|
|
402
|
+
return tokens;
|
|
403
|
+
},
|
|
404
|
+
saveTokens: async (tokens) => {
|
|
405
|
+
tokensRejected = false;
|
|
406
|
+
lastRefreshToken = tokens.refresh_token;
|
|
407
|
+
await update({ tokens });
|
|
408
|
+
},
|
|
409
|
+
discoveryState: async () => (await getMcpServiceAccountCredentials(provider))?.discoveryState,
|
|
410
|
+
saveDiscoveryState: async (discoveryState) => {
|
|
411
|
+
await update({ discoveryState });
|
|
412
|
+
},
|
|
413
|
+
invalidateCredentials: (scope) => {
|
|
414
|
+
if (scope === "all" || scope === "tokens") tokensRejected = true;
|
|
415
|
+
},
|
|
416
|
+
redirectToAuthorization: async () => {
|
|
417
|
+
if (!await withStoreLock(async (fence) => {
|
|
418
|
+
const stored = await getMcpServiceAccountCredentials(provider);
|
|
419
|
+
if (!stored) return true;
|
|
420
|
+
if (!tokensRejected && stored.tokens.refresh_token || stored.tokens.refresh_token !== lastRefreshToken) return false;
|
|
421
|
+
await fence();
|
|
422
|
+
await deleteMcpServiceAccountCredentials(provider);
|
|
423
|
+
logWarn("mcp.service_account.disconnected", { "app.plugin.name": provider });
|
|
424
|
+
return true;
|
|
425
|
+
})) throw new Error(`The ${provider} service account could not refresh its token. Try again later.`);
|
|
426
|
+
throw new McpAuthorizationRequiredError(provider, reconnectMessage(provider));
|
|
427
|
+
},
|
|
428
|
+
saveCodeVerifier: () => void 0,
|
|
429
|
+
codeVerifier: () => {
|
|
430
|
+
throw new Error(reconnectMessage(provider));
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function signInKey(state) {
|
|
435
|
+
return `${SIGN_IN_KEY_PREFIX}:${state}`;
|
|
436
|
+
}
|
|
437
|
+
function parseSignIn(value) {
|
|
438
|
+
if (typeof value !== "string") return;
|
|
439
|
+
try {
|
|
440
|
+
const parsed = JSON.parse(value);
|
|
441
|
+
if (!isRecord(parsed) || !isRecord(parsed.clientInformation) || typeof parsed.codeVerifier !== "string" || typeof parsed.provider !== "string" || typeof parsed.redirectUrl !== "string") return;
|
|
442
|
+
return {
|
|
443
|
+
clientInformation: parsed.clientInformation,
|
|
444
|
+
codeVerifier: parsed.codeVerifier,
|
|
445
|
+
...isRecord(parsed.discoveryState) ? { discoveryState: parsed.discoveryState } : void 0,
|
|
446
|
+
provider: parsed.provider,
|
|
447
|
+
redirectUrl: parsed.redirectUrl,
|
|
448
|
+
...typeof parsed.startedBy === "string" ? { startedBy: parsed.startedBy } : void 0
|
|
449
|
+
};
|
|
450
|
+
} catch {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/** Build an OAuth client provider that keeps sign-in state in memory. */
|
|
455
|
+
function signInProvider(input) {
|
|
456
|
+
const captured = {
|
|
457
|
+
clientInformation: input.pending?.clientInformation,
|
|
458
|
+
codeVerifier: input.pending?.codeVerifier,
|
|
459
|
+
discoveryState: input.pending?.discoveryState
|
|
460
|
+
};
|
|
461
|
+
return {
|
|
462
|
+
captured,
|
|
463
|
+
provider: {
|
|
464
|
+
redirectUrl: input.redirectUrl,
|
|
465
|
+
clientMetadata: clientMetadata(input.redirectUrl),
|
|
466
|
+
...input.state ? { state: () => input.state } : void 0,
|
|
467
|
+
clientInformation: () => captured.clientInformation,
|
|
468
|
+
saveClientInformation: (next) => {
|
|
469
|
+
captured.clientInformation = next;
|
|
470
|
+
},
|
|
471
|
+
tokens: () => captured.tokens,
|
|
472
|
+
saveTokens: (next) => {
|
|
473
|
+
captured.tokens = next;
|
|
474
|
+
},
|
|
475
|
+
discoveryState: () => captured.discoveryState,
|
|
476
|
+
saveDiscoveryState: (next) => {
|
|
477
|
+
captured.discoveryState = next;
|
|
478
|
+
},
|
|
479
|
+
redirectToAuthorization: (url) => {
|
|
480
|
+
captured.authorizationUrl = url;
|
|
481
|
+
},
|
|
482
|
+
saveCodeVerifier: (next) => {
|
|
483
|
+
captured.codeVerifier = next;
|
|
484
|
+
},
|
|
485
|
+
codeVerifier: () => {
|
|
486
|
+
if (!captured.codeVerifier) throw new Error("Missing MCP OAuth code verifier");
|
|
487
|
+
return captured.codeVerifier;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function requireServiceAccountMcp(plugin) {
|
|
493
|
+
const mcp = plugin.manifest.mcp;
|
|
494
|
+
if (!mcp || !plugin.manifest.accounts?.serviceAccount) throw new Error(`Plugin "${plugin.manifest.name}" does not support a service account`);
|
|
495
|
+
return mcp;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Start a service account sign-in and return the provider sign-in URL.
|
|
499
|
+
*
|
|
500
|
+
* The pending sign-in is single use and expires after a short time. Whoever
|
|
501
|
+
* signs in decides which account Junior uses, so only Junior admins may start
|
|
502
|
+
* it.
|
|
503
|
+
*/
|
|
504
|
+
async function startMcpServiceAccountSignIn(input) {
|
|
505
|
+
const mcp = requireServiceAccountMcp(input.plugin);
|
|
506
|
+
const provider = input.plugin.manifest.name;
|
|
507
|
+
const state = randomUUID();
|
|
508
|
+
const signIn = signInProvider({
|
|
509
|
+
redirectUrl: input.redirectUrl,
|
|
510
|
+
state
|
|
511
|
+
});
|
|
512
|
+
if (await auth(signIn.provider, { serverUrl: mcp.url }) !== "REDIRECT") throw new Error(`${provider} did not start an OAuth sign-in`);
|
|
513
|
+
const { authorizationUrl, clientInformation, codeVerifier, discoveryState } = signIn.captured;
|
|
514
|
+
if (!authorizationUrl || !clientInformation || !codeVerifier) throw new Error(`${provider} did not return an authorization URL`);
|
|
515
|
+
await getStateAdapter().set(signInKey(state), JSON.stringify({
|
|
516
|
+
clientInformation,
|
|
517
|
+
codeVerifier,
|
|
518
|
+
...discoveryState ? { discoveryState } : void 0,
|
|
519
|
+
provider,
|
|
520
|
+
redirectUrl: input.redirectUrl,
|
|
521
|
+
...input.startedBy ? { startedBy: input.startedBy } : void 0
|
|
522
|
+
}), SIGN_IN_TTL_MS);
|
|
523
|
+
return { authorizationUrl };
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Finish a service account sign-in from the OAuth callback parameters.
|
|
527
|
+
*
|
|
528
|
+
* It accepts only a pending sign-in for the same plugin that the same Admin
|
|
529
|
+
* started. It then saves the grant as the plugin's service account.
|
|
530
|
+
*/
|
|
531
|
+
async function finishMcpServiceAccountSignIn(input) {
|
|
532
|
+
const mcp = requireServiceAccountMcp(input.plugin);
|
|
533
|
+
const provider = input.plugin.manifest.name;
|
|
534
|
+
const state = input.callback.get("state");
|
|
535
|
+
const pending = state ? parseSignIn(await getStateAdapter().get(signInKey(state))) : void 0;
|
|
536
|
+
if (!state || !pending || pending.provider !== provider || pending.startedBy !== input.connectedBy) return {
|
|
537
|
+
ok: false,
|
|
538
|
+
message: "This sign-in expired or did not start here. Start it again."
|
|
539
|
+
};
|
|
540
|
+
await getStateAdapter().delete(signInKey(state));
|
|
541
|
+
const error = input.callback.get("error");
|
|
542
|
+
if (error) return {
|
|
543
|
+
ok: false,
|
|
544
|
+
message: `${provider} did not finish the sign-in (${error}).`
|
|
545
|
+
};
|
|
546
|
+
const authorizationCode = input.callback.get("code");
|
|
547
|
+
if (!authorizationCode) return {
|
|
548
|
+
ok: false,
|
|
549
|
+
message: `${provider} did not return an authorization code.`
|
|
550
|
+
};
|
|
551
|
+
const signIn = signInProvider({
|
|
552
|
+
pending,
|
|
553
|
+
redirectUrl: pending.redirectUrl
|
|
554
|
+
});
|
|
555
|
+
await auth(signIn.provider, {
|
|
556
|
+
serverUrl: mcp.url,
|
|
557
|
+
authorizationCode
|
|
558
|
+
});
|
|
559
|
+
const { clientInformation, discoveryState, tokens } = signIn.captured;
|
|
560
|
+
if (!clientInformation || !tokens) throw new Error(`${provider} did not issue tokens`);
|
|
561
|
+
const credentials = {
|
|
562
|
+
clientInformation,
|
|
563
|
+
connectedAtMs: Date.now(),
|
|
564
|
+
...input.connectedBy ? { connectedBy: input.connectedBy } : void 0,
|
|
565
|
+
...discoveryState ? { discoveryState } : void 0,
|
|
566
|
+
redirectUrl: pending.redirectUrl,
|
|
567
|
+
tokens
|
|
568
|
+
};
|
|
569
|
+
await putMcpServiceAccountCredentials(provider, credentials);
|
|
570
|
+
return {
|
|
571
|
+
ok: true,
|
|
572
|
+
credentials
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
// src/chat/credentials/service-account.ts
|
|
576
|
+
/**
|
|
577
|
+
* Service accounts.
|
|
578
|
+
*
|
|
579
|
+
* A plugin's `accounts` block can allow an optional install-wide provider
|
|
580
|
+
* account, often a bot, that Junior itself uses. By default, the credential
|
|
581
|
+
* order for one request is:
|
|
582
|
+
*
|
|
583
|
+
* 1. The Actor's own connection, when it exists.
|
|
584
|
+
* 2. The plugin's service account, when an Admin connected it and
|
|
585
|
+
* `accounts.service-account` lists what the request uses.
|
|
586
|
+
* 3. Otherwise, the Actor must connect their own account.
|
|
587
|
+
*
|
|
588
|
+
* With `accounts.user-oauth: false`, Junior uses only the service account.
|
|
589
|
+
* It ignores Actor connections, never asks an Actor to connect, and hides
|
|
590
|
+
* what the account does not list. Until an Admin connects the account, the
|
|
591
|
+
* plugin is not available.
|
|
592
|
+
*
|
|
593
|
+
* This module is the one place that decides which requests the service
|
|
594
|
+
* account may serve. Each request surface asks it before it uses the shared
|
|
595
|
+
* account. Today MCP tools are the only request kind.
|
|
596
|
+
*
|
|
597
|
+
* This module also owns the connect flow that the dashboard Admin page and
|
|
598
|
+
* the `junior service-account` CLI share. Today MCP is the only surface that
|
|
599
|
+
* stores a service account.
|
|
600
|
+
*/
|
|
601
|
+
/** Return whether the plugin's service account may serve this request. */
|
|
602
|
+
function serviceAccountAllows(manifest, request) {
|
|
603
|
+
return manifest.accounts?.serviceAccount?.tools.includes(request.tool) ?? false;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Return whether the plugin uses only its service account.
|
|
607
|
+
*
|
|
608
|
+
* Then no surface may fall back to an Actor's own connection or ask an
|
|
609
|
+
* Actor to connect.
|
|
610
|
+
*/
|
|
611
|
+
function serviceAccountOnly(manifest) {
|
|
612
|
+
return manifest.accounts?.userOAuth === false;
|
|
613
|
+
}
|
|
614
|
+
/** Return the plugins whose `accounts` block allows a service account. */
|
|
615
|
+
function serviceAccountPlugins(plugins) {
|
|
616
|
+
return plugins.filter((plugin) => plugin.manifest.accounts?.serviceAccount);
|
|
617
|
+
}
|
|
618
|
+
/** Read the service account status for one plugin. */
|
|
619
|
+
async function getServiceAccountStatus(plugin) {
|
|
620
|
+
const { manifest } = plugin;
|
|
621
|
+
const serviceAccount = manifest.accounts?.serviceAccount;
|
|
622
|
+
if (!serviceAccount) throw new Error(`Plugin ${manifest.name} does not allow a service account`);
|
|
623
|
+
const credentials = await getMcpServiceAccountCredentials(manifest.name);
|
|
624
|
+
return {
|
|
625
|
+
provider: manifest.name,
|
|
626
|
+
displayName: manifest.displayName,
|
|
627
|
+
userOAuth: !serviceAccountOnly(manifest),
|
|
628
|
+
tools: serviceAccount.tools,
|
|
629
|
+
connected: Boolean(credentials),
|
|
630
|
+
...credentials ? { connectedAt: new Date(credentials.connectedAtMs).toISOString() } : void 0,
|
|
631
|
+
...credentials?.connectedBy ? { connectedBy: credentials.connectedBy } : void 0
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Start signing in to a plugin's provider as its service account.
|
|
636
|
+
*
|
|
637
|
+
* Whoever signs in decides which account Junior uses, so only Junior admins
|
|
638
|
+
* may start it. `startedBy` binds the sign-in to that Admin.
|
|
639
|
+
*/
|
|
640
|
+
async function startServiceAccountSignIn(input) {
|
|
641
|
+
return await startMcpServiceAccountSignIn(input);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Finish a sign-in from its OAuth callback and save the service account.
|
|
645
|
+
*
|
|
646
|
+
* A failure message is safe to show the Admin. Unexpected provider failures
|
|
647
|
+
* throw.
|
|
648
|
+
*/
|
|
649
|
+
async function finishServiceAccountSignIn(input) {
|
|
650
|
+
const result = await finishMcpServiceAccountSignIn(input);
|
|
651
|
+
return result.ok ? { ok: true } : result;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Remove a plugin's service account from Junior.
|
|
655
|
+
*
|
|
656
|
+
* The provider grant stays until someone revokes it in the provider.
|
|
657
|
+
*/
|
|
658
|
+
async function disconnectServiceAccount(plugin) {
|
|
659
|
+
await deleteMcpServiceAccountCredentials(plugin.manifest.name);
|
|
660
|
+
}
|
|
661
|
+
export { getMcpProviderErrorAttributes as _, serviceAccountOnly as a, createMcpServiceAccountClientProvider as c, PluginMcpClient as d, McpProviderError as f, getMcpAwareTelemetryMessage as g, getMcpAwareErrorType as h, serviceAccountAllows as i, getMcpServiceAccountCredentials as l, McpToolError as m, finishServiceAccountSignIn as n, serviceAccountPlugins as o, McpServiceAccountRequiredError as p, getServiceAccountStatus as r, startServiceAccountSignIn as s, disconnectServiceAccount as t, McpAuthorizationRequiredError as u, toMcpProviderError as v };
|