@frontmcp/plugin-approval 0.0.1 → 0.7.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/approval/errors.d.ts +149 -0
- package/approval/errors.d.ts.map +1 -0
- package/approval/factories.d.ts +251 -0
- package/approval/factories.d.ts.map +1 -0
- package/approval/guards.d.ts +61 -0
- package/approval/guards.d.ts.map +1 -0
- package/approval/index.d.ts +43 -0
- package/approval/index.d.ts.map +1 -0
- package/approval/schemas.d.ts +179 -0
- package/approval/schemas.d.ts.map +1 -0
- package/approval/types.d.ts +252 -0
- package/approval/types.d.ts.map +1 -0
- package/approval.context-extension.d.ts +21 -0
- package/approval.context-extension.d.ts.map +1 -0
- package/approval.plugin.d.ts +128 -0
- package/approval.plugin.d.ts.map +1 -0
- package/approval.symbols.d.ts +22 -0
- package/approval.symbols.d.ts.map +1 -0
- package/esm/index.mjs +1228 -0
- package/esm/package.json +66 -0
- package/flows/index.d.ts +9 -0
- package/flows/index.d.ts.map +1 -0
- package/hooks/approval-check.hook.d.ts +25 -0
- package/hooks/approval-check.hook.d.ts.map +1 -0
- package/hooks/index.d.ts +7 -0
- package/hooks/index.d.ts.map +1 -0
- package/index.d.ts +44 -0
- package/index.d.ts.map +1 -0
- package/index.js +1279 -0
- package/package.json +3 -3
- package/services/approval.service.d.ts +85 -0
- package/services/approval.service.d.ts.map +1 -0
- package/services/challenge.service.d.ts +115 -0
- package/services/challenge.service.d.ts.map +1 -0
- package/services/index.d.ts +8 -0
- package/services/index.d.ts.map +1 -0
- package/stores/approval-storage.store.d.ts +71 -0
- package/stores/approval-storage.store.d.ts.map +1 -0
- package/stores/approval-store.interface.d.ts +121 -0
- package/stores/approval-store.interface.d.ts.map +1 -0
- package/stores/index.d.ts +8 -0
- package/stores/index.d.ts.map +1 -0
- package/types/approval.types.d.ts +98 -0
- package/types/approval.types.d.ts.map +1 -0
- package/types/index.d.ts +7 -0
- package/types/index.d.ts.map +1 -0
package/index.js
ADDED
|
@@ -0,0 +1,1279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
21
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
22
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
23
|
+
if (decorator = decorators[i])
|
|
24
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
25
|
+
if (kind && result) __defProp(target, key, result);
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
29
|
+
|
|
30
|
+
// plugins/plugin-approval/src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ApprovalCheckPlugin: () => ApprovalCheckPlugin,
|
|
34
|
+
ApprovalError: () => ApprovalError,
|
|
35
|
+
ApprovalExpiredError: () => ApprovalExpiredError,
|
|
36
|
+
ApprovalOperationError: () => ApprovalOperationError,
|
|
37
|
+
ApprovalPlugin: () => ApprovalPlugin,
|
|
38
|
+
ApprovalPluginClass: () => ApprovalPlugin,
|
|
39
|
+
ApprovalRequiredError: () => ApprovalRequiredError,
|
|
40
|
+
ApprovalScope: () => ApprovalScope,
|
|
41
|
+
ApprovalScopeNotAllowedError: () => ApprovalScopeNotAllowedError,
|
|
42
|
+
ApprovalService: () => ApprovalService,
|
|
43
|
+
ApprovalServiceToken: () => ApprovalServiceToken,
|
|
44
|
+
ApprovalState: () => ApprovalState,
|
|
45
|
+
ApprovalStorageStore: () => ApprovalStorageStore,
|
|
46
|
+
ApprovalStoreToken: () => ApprovalStoreToken,
|
|
47
|
+
ChallengeService: () => ChallengeService,
|
|
48
|
+
ChallengeServiceToken: () => ChallengeServiceToken,
|
|
49
|
+
ChallengeValidationError: () => ChallengeValidationError,
|
|
50
|
+
adminGrantor: () => adminGrantor,
|
|
51
|
+
adminRevoker: () => adminRevoker,
|
|
52
|
+
agentGrantor: () => agentGrantor,
|
|
53
|
+
apiGrantor: () => apiGrantor,
|
|
54
|
+
createApprovalMemoryStore: () => createApprovalMemoryStore,
|
|
55
|
+
createApprovalService: () => createApprovalService,
|
|
56
|
+
createMemoryChallengeService: () => createMemoryChallengeService,
|
|
57
|
+
customGrantor: () => customGrantor,
|
|
58
|
+
expiryRevoker: () => expiryRevoker,
|
|
59
|
+
installApprovalContextExtension: () => installApprovalContextExtension,
|
|
60
|
+
isApiGrantor: () => isApiGrantor,
|
|
61
|
+
isAutoGrantor: () => isAutoGrantor,
|
|
62
|
+
isDelegatedGrantor: () => isDelegatedGrantor,
|
|
63
|
+
isGrantorSource: () => isGrantorSource,
|
|
64
|
+
isHumanGrantor: () => isHumanGrantor,
|
|
65
|
+
normalizeGrantor: () => normalizeGrantor,
|
|
66
|
+
normalizeRevoker: () => normalizeRevoker,
|
|
67
|
+
oauthGrantor: () => oauthGrantor,
|
|
68
|
+
policyGrantor: () => policyGrantor,
|
|
69
|
+
policyRevoker: () => policyRevoker,
|
|
70
|
+
sessionEndRevoker: () => sessionEndRevoker,
|
|
71
|
+
systemGrantor: () => systemGrantor,
|
|
72
|
+
testGrantor: () => testGrantor,
|
|
73
|
+
userGrantor: () => userGrantor,
|
|
74
|
+
userRevoker: () => userRevoker
|
|
75
|
+
});
|
|
76
|
+
module.exports = __toCommonJS(index_exports);
|
|
77
|
+
|
|
78
|
+
// plugins/plugin-approval/src/approval.plugin.ts
|
|
79
|
+
var import_sdk5 = require("@frontmcp/sdk");
|
|
80
|
+
|
|
81
|
+
// plugins/plugin-approval/src/stores/approval-storage.store.ts
|
|
82
|
+
var import_sdk = require("@frontmcp/sdk");
|
|
83
|
+
var import_utils = require("@frontmcp/utils");
|
|
84
|
+
|
|
85
|
+
// plugins/plugin-approval/src/approval/types.ts
|
|
86
|
+
var ApprovalScope = /* @__PURE__ */ ((ApprovalScope2) => {
|
|
87
|
+
ApprovalScope2["SESSION"] = "session";
|
|
88
|
+
ApprovalScope2["USER"] = "user";
|
|
89
|
+
ApprovalScope2["TIME_LIMITED"] = "time_limited";
|
|
90
|
+
ApprovalScope2["TOOL_SPECIFIC"] = "tool_specific";
|
|
91
|
+
ApprovalScope2["CONTEXT_SPECIFIC"] = "context_specific";
|
|
92
|
+
return ApprovalScope2;
|
|
93
|
+
})(ApprovalScope || {});
|
|
94
|
+
var ApprovalState = /* @__PURE__ */ ((ApprovalState2) => {
|
|
95
|
+
ApprovalState2["PENDING"] = "pending";
|
|
96
|
+
ApprovalState2["APPROVED"] = "approved";
|
|
97
|
+
ApprovalState2["DENIED"] = "denied";
|
|
98
|
+
ApprovalState2["EXPIRED"] = "expired";
|
|
99
|
+
return ApprovalState2;
|
|
100
|
+
})(ApprovalState || {});
|
|
101
|
+
|
|
102
|
+
// plugins/plugin-approval/src/approval/schemas.ts
|
|
103
|
+
var import_zod = require("zod");
|
|
104
|
+
var approvalScopeSchema = import_zod.z.nativeEnum(ApprovalScope);
|
|
105
|
+
var approvalStateSchema = import_zod.z.nativeEnum(ApprovalState);
|
|
106
|
+
var approvalMethodSchema = import_zod.z.enum(["interactive", "implicit", "delegation", "batch", "api"]);
|
|
107
|
+
var approvalSourceTypeSchema = import_zod.z.string().min(1);
|
|
108
|
+
var revocationMethodSchema = import_zod.z.enum(["interactive", "implicit", "policy", "expiry"]);
|
|
109
|
+
var approvalCategorySchema = import_zod.z.enum(["read", "write", "delete", "execute", "admin"]);
|
|
110
|
+
var riskLevelSchema = import_zod.z.enum(["low", "medium", "high", "critical"]);
|
|
111
|
+
var approvalContextSchema = import_zod.z.object({
|
|
112
|
+
type: import_zod.z.string().min(1),
|
|
113
|
+
identifier: import_zod.z.string().min(1),
|
|
114
|
+
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
|
|
115
|
+
});
|
|
116
|
+
var delegationContextSchema = import_zod.z.object({
|
|
117
|
+
delegatorId: import_zod.z.string().min(1),
|
|
118
|
+
delegateId: import_zod.z.string().min(1),
|
|
119
|
+
purpose: import_zod.z.string().optional(),
|
|
120
|
+
constraints: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
|
|
121
|
+
});
|
|
122
|
+
var approvalGrantorSchema = import_zod.z.object({
|
|
123
|
+
source: approvalSourceTypeSchema,
|
|
124
|
+
identifier: import_zod.z.string().optional(),
|
|
125
|
+
displayName: import_zod.z.string().optional(),
|
|
126
|
+
method: approvalMethodSchema.optional(),
|
|
127
|
+
origin: import_zod.z.string().optional(),
|
|
128
|
+
delegationContext: delegationContextSchema.optional()
|
|
129
|
+
});
|
|
130
|
+
var approvalRevokerSchema = import_zod.z.object({
|
|
131
|
+
source: import_zod.z.string().min(1),
|
|
132
|
+
identifier: import_zod.z.string().optional(),
|
|
133
|
+
displayName: import_zod.z.string().optional(),
|
|
134
|
+
method: revocationMethodSchema.optional()
|
|
135
|
+
});
|
|
136
|
+
var approvalRecordSchema = import_zod.z.object({
|
|
137
|
+
toolId: import_zod.z.string().min(1),
|
|
138
|
+
state: approvalStateSchema,
|
|
139
|
+
scope: approvalScopeSchema,
|
|
140
|
+
grantedAt: import_zod.z.number(),
|
|
141
|
+
expiresAt: import_zod.z.number().optional(),
|
|
142
|
+
ttlMs: import_zod.z.number().optional(),
|
|
143
|
+
sessionId: import_zod.z.string().optional(),
|
|
144
|
+
userId: import_zod.z.string().optional(),
|
|
145
|
+
context: approvalContextSchema.optional(),
|
|
146
|
+
grantedBy: approvalGrantorSchema,
|
|
147
|
+
approvalChain: import_zod.z.array(approvalGrantorSchema).optional(),
|
|
148
|
+
reason: import_zod.z.string().optional(),
|
|
149
|
+
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
|
|
150
|
+
revokedAt: import_zod.z.number().optional(),
|
|
151
|
+
revokedBy: approvalRevokerSchema.optional(),
|
|
152
|
+
revocationReason: import_zod.z.string().optional()
|
|
153
|
+
});
|
|
154
|
+
var toolApprovalRequirementSchema = import_zod.z.object({
|
|
155
|
+
required: import_zod.z.boolean().optional(),
|
|
156
|
+
defaultScope: approvalScopeSchema.optional(),
|
|
157
|
+
allowedScopes: import_zod.z.array(approvalScopeSchema).optional(),
|
|
158
|
+
maxTtlMs: import_zod.z.number().positive().optional(),
|
|
159
|
+
alwaysPrompt: import_zod.z.boolean().optional(),
|
|
160
|
+
skipApproval: import_zod.z.boolean().optional(),
|
|
161
|
+
approvalMessage: import_zod.z.string().optional(),
|
|
162
|
+
category: approvalCategorySchema.optional(),
|
|
163
|
+
riskLevel: riskLevelSchema.optional(),
|
|
164
|
+
preApprovedContexts: import_zod.z.array(approvalContextSchema).optional()
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// plugins/plugin-approval/src/approval/factories.ts
|
|
168
|
+
function userGrantor(userId, displayName, options) {
|
|
169
|
+
return {
|
|
170
|
+
source: "user",
|
|
171
|
+
identifier: userId,
|
|
172
|
+
displayName,
|
|
173
|
+
method: options?.method ?? "interactive",
|
|
174
|
+
origin: options?.origin
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function policyGrantor(policyId, policyName) {
|
|
178
|
+
return {
|
|
179
|
+
source: "policy",
|
|
180
|
+
identifier: policyId,
|
|
181
|
+
displayName: policyName,
|
|
182
|
+
method: "implicit"
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function adminGrantor(adminId, displayName, options) {
|
|
186
|
+
return {
|
|
187
|
+
source: "admin",
|
|
188
|
+
identifier: adminId,
|
|
189
|
+
displayName,
|
|
190
|
+
method: options?.method ?? "interactive",
|
|
191
|
+
origin: options?.origin
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function systemGrantor(systemId = "system") {
|
|
195
|
+
return {
|
|
196
|
+
source: "system",
|
|
197
|
+
identifier: systemId,
|
|
198
|
+
method: "implicit"
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function agentGrantor(agentId, delegationContext, displayName) {
|
|
202
|
+
return {
|
|
203
|
+
source: "agent",
|
|
204
|
+
identifier: agentId,
|
|
205
|
+
displayName,
|
|
206
|
+
method: "delegation",
|
|
207
|
+
delegationContext
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function apiGrantor(apiKeyPrefix, serviceName) {
|
|
211
|
+
return {
|
|
212
|
+
source: "api",
|
|
213
|
+
identifier: apiKeyPrefix,
|
|
214
|
+
displayName: serviceName,
|
|
215
|
+
method: "api",
|
|
216
|
+
origin: "api"
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function oauthGrantor(tokenId, provider) {
|
|
220
|
+
return {
|
|
221
|
+
source: "oauth",
|
|
222
|
+
identifier: tokenId,
|
|
223
|
+
displayName: provider,
|
|
224
|
+
method: "api",
|
|
225
|
+
origin: "oauth"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function testGrantor() {
|
|
229
|
+
return {
|
|
230
|
+
source: "test",
|
|
231
|
+
identifier: "test",
|
|
232
|
+
method: "implicit"
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function customGrantor(source, identifier, options) {
|
|
236
|
+
return {
|
|
237
|
+
source,
|
|
238
|
+
identifier,
|
|
239
|
+
displayName: options?.displayName,
|
|
240
|
+
method: options?.method,
|
|
241
|
+
origin: options?.origin,
|
|
242
|
+
delegationContext: options?.delegationContext
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function userRevoker(userId, displayName) {
|
|
246
|
+
return {
|
|
247
|
+
source: "user",
|
|
248
|
+
identifier: userId,
|
|
249
|
+
displayName,
|
|
250
|
+
method: "interactive"
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function adminRevoker(adminId, displayName) {
|
|
254
|
+
return {
|
|
255
|
+
source: "admin",
|
|
256
|
+
identifier: adminId,
|
|
257
|
+
displayName,
|
|
258
|
+
method: "interactive"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function expiryRevoker() {
|
|
262
|
+
return {
|
|
263
|
+
source: "expiry",
|
|
264
|
+
method: "expiry"
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function sessionEndRevoker(sessionId) {
|
|
268
|
+
return {
|
|
269
|
+
source: "session_end",
|
|
270
|
+
identifier: sessionId,
|
|
271
|
+
method: "implicit"
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function policyRevoker(policyId, policyName) {
|
|
275
|
+
return {
|
|
276
|
+
source: "policy",
|
|
277
|
+
identifier: policyId,
|
|
278
|
+
displayName: policyName,
|
|
279
|
+
method: "policy"
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function normalizeGrantor(input) {
|
|
283
|
+
if (!input) {
|
|
284
|
+
return { source: "user" };
|
|
285
|
+
}
|
|
286
|
+
if (typeof input === "string") {
|
|
287
|
+
return { source: input };
|
|
288
|
+
}
|
|
289
|
+
return input;
|
|
290
|
+
}
|
|
291
|
+
function normalizeRevoker(input) {
|
|
292
|
+
if (!input) {
|
|
293
|
+
return { source: "user" };
|
|
294
|
+
}
|
|
295
|
+
if (typeof input === "string") {
|
|
296
|
+
return { source: input };
|
|
297
|
+
}
|
|
298
|
+
return input;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// plugins/plugin-approval/src/approval/guards.ts
|
|
302
|
+
function isGrantorSource(grantor, source) {
|
|
303
|
+
return grantor.source === source;
|
|
304
|
+
}
|
|
305
|
+
function isHumanGrantor(grantor) {
|
|
306
|
+
return grantor.source === "user" || grantor.source === "admin";
|
|
307
|
+
}
|
|
308
|
+
function isAutoGrantor(grantor) {
|
|
309
|
+
return grantor.source === "policy" || grantor.source === "system" || grantor.source === "test";
|
|
310
|
+
}
|
|
311
|
+
function isDelegatedGrantor(grantor) {
|
|
312
|
+
return grantor.source === "agent" && !!grantor.delegationContext;
|
|
313
|
+
}
|
|
314
|
+
function isApiGrantor(grantor) {
|
|
315
|
+
return grantor.source === "api" || grantor.source === "oauth";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// plugins/plugin-approval/src/approval/errors.ts
|
|
319
|
+
var ApprovalError = class extends Error {
|
|
320
|
+
constructor(message) {
|
|
321
|
+
super(message);
|
|
322
|
+
this.name = "ApprovalError";
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
var ApprovalRequiredError = class extends ApprovalError {
|
|
326
|
+
constructor(details) {
|
|
327
|
+
super(details.message);
|
|
328
|
+
this.details = details;
|
|
329
|
+
this.name = "ApprovalRequiredError";
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Convert to a JSON-RPC compatible error structure.
|
|
333
|
+
*/
|
|
334
|
+
toJsonRpcError() {
|
|
335
|
+
return {
|
|
336
|
+
code: -32600,
|
|
337
|
+
// Invalid Request
|
|
338
|
+
message: this.details.message,
|
|
339
|
+
data: {
|
|
340
|
+
type: "approval_required",
|
|
341
|
+
toolId: this.details.toolId,
|
|
342
|
+
state: this.details.state,
|
|
343
|
+
options: this.details.approvalOptions
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
var ApprovalOperationError = class extends ApprovalError {
|
|
349
|
+
constructor(operation, reason) {
|
|
350
|
+
super(`Approval ${operation} failed: ${reason}`);
|
|
351
|
+
this.operation = operation;
|
|
352
|
+
this.reason = reason;
|
|
353
|
+
this.name = "ApprovalOperationError";
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Convert to a JSON-RPC compatible error structure.
|
|
357
|
+
*/
|
|
358
|
+
toJsonRpcError() {
|
|
359
|
+
return {
|
|
360
|
+
code: -32603,
|
|
361
|
+
// Internal Error
|
|
362
|
+
message: "Approval operation failed",
|
|
363
|
+
data: {
|
|
364
|
+
type: "approval_operation_error",
|
|
365
|
+
operation: this.operation
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
var ApprovalScopeNotAllowedError = class extends ApprovalError {
|
|
371
|
+
constructor(requestedScope, allowedScopes) {
|
|
372
|
+
super(
|
|
373
|
+
`Approval scope '${requestedScope}' is not allowed for this tool. Allowed scopes: ${allowedScopes.join(", ")}`
|
|
374
|
+
);
|
|
375
|
+
this.requestedScope = requestedScope;
|
|
376
|
+
this.allowedScopes = allowedScopes;
|
|
377
|
+
this.name = "ApprovalScopeNotAllowedError";
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Convert to a JSON-RPC compatible error structure.
|
|
381
|
+
*/
|
|
382
|
+
toJsonRpcError() {
|
|
383
|
+
return {
|
|
384
|
+
code: -32602,
|
|
385
|
+
// Invalid Params
|
|
386
|
+
message: this.message,
|
|
387
|
+
data: {
|
|
388
|
+
type: "approval_scope_not_allowed",
|
|
389
|
+
requestedScope: this.requestedScope,
|
|
390
|
+
allowedScopes: this.allowedScopes
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
var ApprovalExpiredError = class extends ApprovalError {
|
|
396
|
+
constructor(toolId, expiredAt) {
|
|
397
|
+
super(`Approval for tool '${toolId}' expired at ${new Date(expiredAt).toISOString()}`);
|
|
398
|
+
this.toolId = toolId;
|
|
399
|
+
this.expiredAt = expiredAt;
|
|
400
|
+
this.name = "ApprovalExpiredError";
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Convert to a JSON-RPC compatible error structure.
|
|
404
|
+
*/
|
|
405
|
+
toJsonRpcError() {
|
|
406
|
+
return {
|
|
407
|
+
code: -32600,
|
|
408
|
+
// Invalid Request
|
|
409
|
+
message: this.message,
|
|
410
|
+
data: {
|
|
411
|
+
type: "approval_expired",
|
|
412
|
+
toolId: this.toolId,
|
|
413
|
+
expiredAt: this.expiredAt
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
var ChallengeValidationError = class extends ApprovalError {
|
|
419
|
+
constructor(reason = "invalid", message) {
|
|
420
|
+
super(message ?? `PKCE challenge validation failed: ${reason}`);
|
|
421
|
+
this.reason = reason;
|
|
422
|
+
this.name = "ChallengeValidationError";
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Convert to a JSON-RPC compatible error structure.
|
|
426
|
+
*/
|
|
427
|
+
toJsonRpcError() {
|
|
428
|
+
return {
|
|
429
|
+
code: -32600,
|
|
430
|
+
// Invalid Request
|
|
431
|
+
message: this.message,
|
|
432
|
+
data: {
|
|
433
|
+
type: "challenge_validation_error",
|
|
434
|
+
reason: this.reason
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// plugins/plugin-approval/src/stores/approval-storage.store.ts
|
|
441
|
+
function escapePattern(str) {
|
|
442
|
+
return str.replace(/[*?[\]\\]/g, "\\$&");
|
|
443
|
+
}
|
|
444
|
+
var ApprovalStorageStore = class {
|
|
445
|
+
storage;
|
|
446
|
+
options;
|
|
447
|
+
cleanupInterval;
|
|
448
|
+
initialized = false;
|
|
449
|
+
ownedStorage = false;
|
|
450
|
+
constructor(options = {}) {
|
|
451
|
+
this.options = {
|
|
452
|
+
storage: options.storage ?? { type: "auto" },
|
|
453
|
+
storageInstance: options.storageInstance,
|
|
454
|
+
namespace: options.namespace ?? "approval",
|
|
455
|
+
cleanupIntervalSeconds: options.cleanupIntervalSeconds ?? 60
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Initialize the storage connection.
|
|
460
|
+
*/
|
|
461
|
+
async initialize() {
|
|
462
|
+
if (this.initialized) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (this.options.storageInstance) {
|
|
466
|
+
this.storage = this.options.storageInstance.namespace(this.options.namespace);
|
|
467
|
+
this.ownedStorage = false;
|
|
468
|
+
} else {
|
|
469
|
+
const rootStorage = await (0, import_utils.createStorage)(this.options.storage);
|
|
470
|
+
this.storage = rootStorage.namespace(this.options.namespace);
|
|
471
|
+
this.ownedStorage = true;
|
|
472
|
+
}
|
|
473
|
+
if (this.options.cleanupIntervalSeconds > 0) {
|
|
474
|
+
this.cleanupInterval = setInterval(() => {
|
|
475
|
+
void this.clearExpiredApprovals();
|
|
476
|
+
}, this.options.cleanupIntervalSeconds * 1e3);
|
|
477
|
+
this.cleanupInterval.unref?.();
|
|
478
|
+
}
|
|
479
|
+
this.initialized = true;
|
|
480
|
+
}
|
|
481
|
+
ensureInitialized() {
|
|
482
|
+
if (!this.initialized) {
|
|
483
|
+
throw new Error("ApprovalStorageStore not initialized. Call initialize() first.");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
buildKey(toolId, sessionId, userId, context) {
|
|
487
|
+
const parts = [toolId];
|
|
488
|
+
if (sessionId) parts.push(`session:${sessionId}`);
|
|
489
|
+
if (userId) parts.push(`user:${userId}`);
|
|
490
|
+
if (context) parts.push(`ctx:${context.type}:${context.identifier}`);
|
|
491
|
+
return parts.join(":");
|
|
492
|
+
}
|
|
493
|
+
parseRecord(value) {
|
|
494
|
+
if (!value) return void 0;
|
|
495
|
+
try {
|
|
496
|
+
const parsed = JSON.parse(value);
|
|
497
|
+
const result = approvalRecordSchema.safeParse(parsed);
|
|
498
|
+
if (!result.success) {
|
|
499
|
+
return void 0;
|
|
500
|
+
}
|
|
501
|
+
return result.data;
|
|
502
|
+
} catch {
|
|
503
|
+
return void 0;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
isExpired(approval) {
|
|
507
|
+
return approval.expiresAt !== void 0 && Date.now() > approval.expiresAt;
|
|
508
|
+
}
|
|
509
|
+
async getApproval(toolId, sessionId, userId) {
|
|
510
|
+
this.ensureInitialized();
|
|
511
|
+
const sessionKey = this.buildKey(toolId, sessionId);
|
|
512
|
+
const sessionValue = await this.storage.get(sessionKey);
|
|
513
|
+
const sessionApproval = this.parseRecord(sessionValue);
|
|
514
|
+
if (sessionApproval && !this.isExpired(sessionApproval)) {
|
|
515
|
+
return sessionApproval;
|
|
516
|
+
}
|
|
517
|
+
if (userId) {
|
|
518
|
+
const userKey = this.buildKey(toolId, void 0, userId);
|
|
519
|
+
const userValue = await this.storage.get(userKey);
|
|
520
|
+
const userApproval = this.parseRecord(userValue);
|
|
521
|
+
if (userApproval && !this.isExpired(userApproval)) {
|
|
522
|
+
return userApproval;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
async queryApprovals(query) {
|
|
528
|
+
this.ensureInitialized();
|
|
529
|
+
const results = [];
|
|
530
|
+
const pattern = query.toolId ? `${query.toolId}:*` : "*";
|
|
531
|
+
const keys = await this.storage.keys(pattern);
|
|
532
|
+
const values = await this.storage.mget(keys);
|
|
533
|
+
for (const value of values) {
|
|
534
|
+
const approval = this.parseRecord(value);
|
|
535
|
+
if (!approval) continue;
|
|
536
|
+
if (!query.includeExpired && this.isExpired(approval)) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (query.toolId && approval.toolId !== query.toolId) continue;
|
|
540
|
+
if (query.toolIds && !query.toolIds.includes(approval.toolId)) continue;
|
|
541
|
+
if (query.scope && approval.scope !== query.scope) continue;
|
|
542
|
+
if (query.scopes && !query.scopes.includes(approval.scope)) continue;
|
|
543
|
+
if (query.state && approval.state !== query.state) continue;
|
|
544
|
+
if (query.states && !query.states.includes(approval.state)) continue;
|
|
545
|
+
if (query.sessionId && approval.sessionId !== query.sessionId) continue;
|
|
546
|
+
if (query.userId && approval.userId !== query.userId) continue;
|
|
547
|
+
if (query.context) {
|
|
548
|
+
if (!approval.context || approval.context.type !== query.context.type || approval.context.identifier !== query.context.identifier) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
results.push(approval);
|
|
553
|
+
}
|
|
554
|
+
return results;
|
|
555
|
+
}
|
|
556
|
+
async grantApproval(options) {
|
|
557
|
+
this.ensureInitialized();
|
|
558
|
+
const now = Date.now();
|
|
559
|
+
const expiresAt = options.ttlMs ? now + options.ttlMs : void 0;
|
|
560
|
+
const grantedBy = normalizeGrantor(options.grantedBy);
|
|
561
|
+
const record = {
|
|
562
|
+
toolId: options.toolId,
|
|
563
|
+
state: "approved" /* APPROVED */,
|
|
564
|
+
scope: options.scope,
|
|
565
|
+
grantedAt: now,
|
|
566
|
+
expiresAt,
|
|
567
|
+
ttlMs: options.ttlMs,
|
|
568
|
+
sessionId: options.sessionId,
|
|
569
|
+
userId: options.userId,
|
|
570
|
+
context: options.context,
|
|
571
|
+
grantedBy,
|
|
572
|
+
reason: options.reason,
|
|
573
|
+
metadata: options.metadata
|
|
574
|
+
};
|
|
575
|
+
const key = this.buildKey(options.toolId, options.sessionId, options.userId, options.context);
|
|
576
|
+
const ttlSeconds = options.ttlMs ? Math.ceil(options.ttlMs / 1e3) : void 0;
|
|
577
|
+
await this.storage.set(key, JSON.stringify(record), { ttlSeconds });
|
|
578
|
+
return record;
|
|
579
|
+
}
|
|
580
|
+
async revokeApproval(options) {
|
|
581
|
+
this.ensureInitialized();
|
|
582
|
+
const key = this.buildKey(options.toolId, options.sessionId, options.userId, options.context);
|
|
583
|
+
const exists = await this.storage.exists(key);
|
|
584
|
+
if (exists) {
|
|
585
|
+
await this.storage.delete(key);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
async isApproved(toolId, sessionId, userId, context) {
|
|
591
|
+
this.ensureInitialized();
|
|
592
|
+
if (context) {
|
|
593
|
+
const contextKey = this.buildKey(toolId, sessionId, userId, context);
|
|
594
|
+
const contextValue = await this.storage.get(contextKey);
|
|
595
|
+
const contextApproval = this.parseRecord(contextValue);
|
|
596
|
+
if (contextApproval && contextApproval.state === "approved" /* APPROVED */ && !this.isExpired(contextApproval)) {
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const sessionKey = this.buildKey(toolId, sessionId);
|
|
601
|
+
const sessionValue = await this.storage.get(sessionKey);
|
|
602
|
+
const sessionApproval = this.parseRecord(sessionValue);
|
|
603
|
+
if (sessionApproval && sessionApproval.state === "approved" /* APPROVED */ && !this.isExpired(sessionApproval)) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
if (userId) {
|
|
607
|
+
const userKey = this.buildKey(toolId, void 0, userId);
|
|
608
|
+
const userValue = await this.storage.get(userKey);
|
|
609
|
+
const userApproval = this.parseRecord(userValue);
|
|
610
|
+
if (userApproval && userApproval.state === "approved" /* APPROVED */ && !this.isExpired(userApproval)) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
async clearSessionApprovals(sessionId) {
|
|
617
|
+
this.ensureInitialized();
|
|
618
|
+
const escapedSessionId = escapePattern(sessionId);
|
|
619
|
+
const pattern = `*:session:${escapedSessionId}*`;
|
|
620
|
+
const keys = await this.storage.keys(pattern);
|
|
621
|
+
if (keys.length === 0) {
|
|
622
|
+
return 0;
|
|
623
|
+
}
|
|
624
|
+
return await this.storage.mdelete(keys);
|
|
625
|
+
}
|
|
626
|
+
async clearExpiredApprovals() {
|
|
627
|
+
this.ensureInitialized();
|
|
628
|
+
const now = Date.now();
|
|
629
|
+
const keys = await this.storage.keys("*");
|
|
630
|
+
const values = await this.storage.mget(keys);
|
|
631
|
+
const keysToDelete = [];
|
|
632
|
+
for (let i = 0; i < keys.length; i++) {
|
|
633
|
+
const approval = this.parseRecord(values[i]);
|
|
634
|
+
if (approval && approval.expiresAt && approval.expiresAt <= now) {
|
|
635
|
+
keysToDelete.push(keys[i]);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (keysToDelete.length > 0) {
|
|
639
|
+
return await this.storage.mdelete(keysToDelete);
|
|
640
|
+
}
|
|
641
|
+
return 0;
|
|
642
|
+
}
|
|
643
|
+
async getStats() {
|
|
644
|
+
this.ensureInitialized();
|
|
645
|
+
const byScope = {
|
|
646
|
+
["session" /* SESSION */]: 0,
|
|
647
|
+
["user" /* USER */]: 0,
|
|
648
|
+
["time_limited" /* TIME_LIMITED */]: 0,
|
|
649
|
+
["tool_specific" /* TOOL_SPECIFIC */]: 0,
|
|
650
|
+
["context_specific" /* CONTEXT_SPECIFIC */]: 0
|
|
651
|
+
};
|
|
652
|
+
const byState = {
|
|
653
|
+
["pending" /* PENDING */]: 0,
|
|
654
|
+
["approved" /* APPROVED */]: 0,
|
|
655
|
+
["denied" /* DENIED */]: 0,
|
|
656
|
+
["expired" /* EXPIRED */]: 0
|
|
657
|
+
};
|
|
658
|
+
const keys = await this.storage.keys("*");
|
|
659
|
+
const values = await this.storage.mget(keys);
|
|
660
|
+
let total = 0;
|
|
661
|
+
for (const value of values) {
|
|
662
|
+
const approval = this.parseRecord(value);
|
|
663
|
+
if (approval) {
|
|
664
|
+
total++;
|
|
665
|
+
byScope[approval.scope]++;
|
|
666
|
+
byState[approval.state]++;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
totalApprovals: total,
|
|
671
|
+
byScope,
|
|
672
|
+
byState
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
async close() {
|
|
676
|
+
if (this.cleanupInterval) {
|
|
677
|
+
clearInterval(this.cleanupInterval);
|
|
678
|
+
this.cleanupInterval = void 0;
|
|
679
|
+
}
|
|
680
|
+
if (this.ownedStorage && this.storage) {
|
|
681
|
+
await this.storage.root.disconnect();
|
|
682
|
+
}
|
|
683
|
+
this.initialized = false;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
ApprovalStorageStore = __decorateClass([
|
|
687
|
+
(0, import_sdk.Provider)({
|
|
688
|
+
name: "provider:approval:store:storage",
|
|
689
|
+
description: "Storage-backed approval store (supports Memory, Redis, Vercel KV, Upstash)",
|
|
690
|
+
scope: import_sdk.ProviderScope.GLOBAL
|
|
691
|
+
})
|
|
692
|
+
], ApprovalStorageStore);
|
|
693
|
+
function createApprovalMemoryStore(options = {}) {
|
|
694
|
+
const memoryStorage = (0, import_utils.createMemoryStorage)();
|
|
695
|
+
return new ApprovalStorageStore({
|
|
696
|
+
...options,
|
|
697
|
+
storageInstance: memoryStorage
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// plugins/plugin-approval/src/services/approval.service.ts
|
|
702
|
+
var import_sdk2 = require("@frontmcp/sdk");
|
|
703
|
+
var ApprovalService = class {
|
|
704
|
+
constructor(store, sessionId, userId) {
|
|
705
|
+
this.store = store;
|
|
706
|
+
this.sessionId = sessionId;
|
|
707
|
+
this.userId = userId;
|
|
708
|
+
}
|
|
709
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
710
|
+
// Query Methods
|
|
711
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
712
|
+
/**
|
|
713
|
+
* Check if a tool is approved for current session/user.
|
|
714
|
+
*/
|
|
715
|
+
async isApproved(toolId, context) {
|
|
716
|
+
return this.store.isApproved(toolId, this.sessionId, this.userId, context);
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Get approval record for a tool.
|
|
720
|
+
*/
|
|
721
|
+
async getApproval(toolId) {
|
|
722
|
+
return this.store.getApproval(toolId, this.sessionId, this.userId);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Get all approvals for current session.
|
|
726
|
+
*/
|
|
727
|
+
async getSessionApprovals() {
|
|
728
|
+
return this.store.queryApprovals({
|
|
729
|
+
sessionId: this.sessionId,
|
|
730
|
+
states: ["approved" /* APPROVED */],
|
|
731
|
+
includeExpired: false
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Get all approvals for current user (across sessions).
|
|
736
|
+
*/
|
|
737
|
+
async getUserApprovals() {
|
|
738
|
+
if (!this.userId) return [];
|
|
739
|
+
return this.store.queryApprovals({
|
|
740
|
+
userId: this.userId,
|
|
741
|
+
scope: "user" /* USER */,
|
|
742
|
+
states: ["approved" /* APPROVED */],
|
|
743
|
+
includeExpired: false
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Query approvals with custom filters.
|
|
748
|
+
*/
|
|
749
|
+
async queryApprovals(query) {
|
|
750
|
+
return this.store.queryApprovals({
|
|
751
|
+
...query,
|
|
752
|
+
sessionId: query.sessionId ?? this.sessionId,
|
|
753
|
+
userId: query.userId ?? this.userId
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
757
|
+
// Grant Methods
|
|
758
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
759
|
+
/**
|
|
760
|
+
* Grant session-scoped approval for a tool.
|
|
761
|
+
*/
|
|
762
|
+
async grantSessionApproval(toolId, options = {}) {
|
|
763
|
+
return this.store.grantApproval({
|
|
764
|
+
toolId,
|
|
765
|
+
scope: "session" /* SESSION */,
|
|
766
|
+
sessionId: this.sessionId,
|
|
767
|
+
grantedBy: options.grantedBy ?? "policy",
|
|
768
|
+
reason: options.reason,
|
|
769
|
+
metadata: options.metadata
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Grant user-scoped approval for a tool.
|
|
774
|
+
*/
|
|
775
|
+
async grantUserApproval(toolId, options = {}) {
|
|
776
|
+
if (!this.userId) {
|
|
777
|
+
throw new Error("Cannot grant user approval without userId");
|
|
778
|
+
}
|
|
779
|
+
return this.store.grantApproval({
|
|
780
|
+
toolId,
|
|
781
|
+
scope: "user" /* USER */,
|
|
782
|
+
userId: this.userId,
|
|
783
|
+
grantedBy: options.grantedBy ?? "policy",
|
|
784
|
+
reason: options.reason,
|
|
785
|
+
metadata: options.metadata
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Grant time-limited approval for a tool.
|
|
790
|
+
*/
|
|
791
|
+
async grantTimeLimitedApproval(toolId, ttlMs, options = {}) {
|
|
792
|
+
return this.store.grantApproval({
|
|
793
|
+
toolId,
|
|
794
|
+
scope: "time_limited" /* TIME_LIMITED */,
|
|
795
|
+
ttlMs,
|
|
796
|
+
sessionId: this.sessionId,
|
|
797
|
+
userId: this.userId,
|
|
798
|
+
grantedBy: options.grantedBy ?? "policy",
|
|
799
|
+
reason: options.reason,
|
|
800
|
+
metadata: options.metadata
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Grant context-specific approval for a tool.
|
|
805
|
+
*/
|
|
806
|
+
async grantContextApproval(toolId, context, options = {}) {
|
|
807
|
+
return this.store.grantApproval({
|
|
808
|
+
toolId,
|
|
809
|
+
scope: "context_specific" /* CONTEXT_SPECIFIC */,
|
|
810
|
+
context,
|
|
811
|
+
sessionId: this.sessionId,
|
|
812
|
+
userId: this.userId,
|
|
813
|
+
grantedBy: options.grantedBy ?? "policy",
|
|
814
|
+
reason: options.reason,
|
|
815
|
+
metadata: options.metadata
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
819
|
+
// Revoke Methods
|
|
820
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
821
|
+
/**
|
|
822
|
+
* Revoke approval for a tool.
|
|
823
|
+
*/
|
|
824
|
+
async revokeApproval(toolId, options = {}) {
|
|
825
|
+
return this.store.revokeApproval({
|
|
826
|
+
toolId,
|
|
827
|
+
sessionId: this.sessionId,
|
|
828
|
+
userId: this.userId,
|
|
829
|
+
revokedBy: options.revokedBy ?? "policy",
|
|
830
|
+
reason: options.reason
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Clear all session approvals.
|
|
835
|
+
*/
|
|
836
|
+
async clearSessionApprovals() {
|
|
837
|
+
return this.store.clearSessionApprovals(this.sessionId);
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
ApprovalService = __decorateClass([
|
|
841
|
+
(0, import_sdk2.Provider)({
|
|
842
|
+
name: "provider:approval:service",
|
|
843
|
+
description: "Service for managing tool approvals",
|
|
844
|
+
scope: import_sdk2.ProviderScope.CONTEXT
|
|
845
|
+
})
|
|
846
|
+
], ApprovalService);
|
|
847
|
+
function createApprovalService(store, sessionId, userId) {
|
|
848
|
+
return new ApprovalService(store, sessionId, userId);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// plugins/plugin-approval/src/services/challenge.service.ts
|
|
852
|
+
var import_sdk3 = require("@frontmcp/sdk");
|
|
853
|
+
var import_utils2 = require("@frontmcp/utils");
|
|
854
|
+
var ChallengeService = class {
|
|
855
|
+
storage;
|
|
856
|
+
options;
|
|
857
|
+
initialized = false;
|
|
858
|
+
ownedStorage = false;
|
|
859
|
+
constructor(options = {}) {
|
|
860
|
+
this.options = {
|
|
861
|
+
storage: options.storage ?? { type: "auto" },
|
|
862
|
+
storageInstance: options.storageInstance,
|
|
863
|
+
namespace: options.namespace ?? "approval:challenge",
|
|
864
|
+
defaultTtlSeconds: options.defaultTtlSeconds ?? 300
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Initialize the service.
|
|
869
|
+
*/
|
|
870
|
+
async initialize() {
|
|
871
|
+
if (this.initialized) return;
|
|
872
|
+
if (this.options.storageInstance) {
|
|
873
|
+
this.storage = this.options.storageInstance.namespace(this.options.namespace);
|
|
874
|
+
this.ownedStorage = false;
|
|
875
|
+
} else {
|
|
876
|
+
const rootStorage = await (0, import_utils2.createStorage)(this.options.storage);
|
|
877
|
+
this.storage = rootStorage.namespace(this.options.namespace);
|
|
878
|
+
this.ownedStorage = true;
|
|
879
|
+
}
|
|
880
|
+
this.initialized = true;
|
|
881
|
+
}
|
|
882
|
+
ensureInitialized() {
|
|
883
|
+
if (!this.initialized) {
|
|
884
|
+
throw new Error("ChallengeService not initialized. Call initialize() first.");
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Create a new PKCE challenge for a tool approval request.
|
|
889
|
+
*
|
|
890
|
+
* @returns Object containing code_verifier (keep secret) and code_challenge (send to webhook)
|
|
891
|
+
*/
|
|
892
|
+
async createChallenge(options) {
|
|
893
|
+
this.ensureInitialized();
|
|
894
|
+
const { codeVerifier, codeChallenge } = (0, import_utils2.generatePkcePair)();
|
|
895
|
+
const now = Date.now();
|
|
896
|
+
const ttlSeconds = options.ttlSeconds ?? this.options.defaultTtlSeconds;
|
|
897
|
+
const expiresAt = now + ttlSeconds * 1e3;
|
|
898
|
+
const record = {
|
|
899
|
+
toolId: options.toolId,
|
|
900
|
+
sessionId: options.sessionId,
|
|
901
|
+
userId: options.userId,
|
|
902
|
+
requestedScope: options.requestedScope,
|
|
903
|
+
requestInfo: options.requestInfo,
|
|
904
|
+
createdAt: now,
|
|
905
|
+
expiresAt,
|
|
906
|
+
webhookSent: false
|
|
907
|
+
};
|
|
908
|
+
await this.storage.set(codeChallenge, JSON.stringify(record), { ttlSeconds });
|
|
909
|
+
return { codeVerifier, codeChallenge, expiresAt };
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Verify a code verifier and retrieve the challenge record.
|
|
913
|
+
*
|
|
914
|
+
* @throws ChallengeValidationError if verification fails
|
|
915
|
+
*/
|
|
916
|
+
async verifyAndConsume(codeVerifier) {
|
|
917
|
+
this.ensureInitialized();
|
|
918
|
+
const { codeChallenge } = generatePkcePairFromVerifier(codeVerifier);
|
|
919
|
+
const recordJson = await this.storage.get(codeChallenge);
|
|
920
|
+
if (!recordJson) {
|
|
921
|
+
throw new ChallengeValidationError("not_found", "Invalid or expired challenge");
|
|
922
|
+
}
|
|
923
|
+
const record = JSON.parse(recordJson);
|
|
924
|
+
if (Date.now() > record.expiresAt) {
|
|
925
|
+
await this.storage.delete(codeChallenge);
|
|
926
|
+
throw new ChallengeValidationError("expired", "Challenge expired");
|
|
927
|
+
}
|
|
928
|
+
await this.storage.delete(codeChallenge);
|
|
929
|
+
return record;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Mark a challenge as having been sent to webhook.
|
|
933
|
+
*/
|
|
934
|
+
async markWebhookSent(codeChallenge) {
|
|
935
|
+
this.ensureInitialized();
|
|
936
|
+
const recordJson = await this.storage.get(codeChallenge);
|
|
937
|
+
if (!recordJson) return false;
|
|
938
|
+
const record = JSON.parse(recordJson);
|
|
939
|
+
record.webhookSent = true;
|
|
940
|
+
const remainingMs = record.expiresAt - Date.now();
|
|
941
|
+
if (remainingMs <= 0) {
|
|
942
|
+
await this.storage.delete(codeChallenge);
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
const ttlSeconds = Math.ceil(remainingMs / 1e3);
|
|
946
|
+
await this.storage.set(codeChallenge, JSON.stringify(record), { ttlSeconds });
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Get a challenge record without consuming it.
|
|
951
|
+
*/
|
|
952
|
+
async getChallenge(codeChallenge) {
|
|
953
|
+
this.ensureInitialized();
|
|
954
|
+
const recordJson = await this.storage.get(codeChallenge);
|
|
955
|
+
if (!recordJson) return null;
|
|
956
|
+
const record = JSON.parse(recordJson);
|
|
957
|
+
if (Date.now() > record.expiresAt) {
|
|
958
|
+
await this.storage.delete(codeChallenge);
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
return record;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Delete a challenge.
|
|
965
|
+
*/
|
|
966
|
+
async deleteChallenge(codeChallenge) {
|
|
967
|
+
this.ensureInitialized();
|
|
968
|
+
const exists = await this.storage.exists(codeChallenge);
|
|
969
|
+
if (exists) {
|
|
970
|
+
await this.storage.delete(codeChallenge);
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Close the service.
|
|
977
|
+
*/
|
|
978
|
+
async close() {
|
|
979
|
+
if (this.ownedStorage && this.storage) {
|
|
980
|
+
await this.storage.root.disconnect();
|
|
981
|
+
}
|
|
982
|
+
this.initialized = false;
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
ChallengeService = __decorateClass([
|
|
986
|
+
(0, import_sdk3.Provider)({
|
|
987
|
+
name: "provider:approval:challenge-service",
|
|
988
|
+
description: "PKCE challenge service for webhook approval flows",
|
|
989
|
+
scope: import_sdk3.ProviderScope.GLOBAL
|
|
990
|
+
})
|
|
991
|
+
], ChallengeService);
|
|
992
|
+
function generatePkcePairFromVerifier(codeVerifier) {
|
|
993
|
+
return {
|
|
994
|
+
codeVerifier,
|
|
995
|
+
codeChallenge: (0, import_utils2.generateCodeChallenge)(codeVerifier)
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function createMemoryChallengeService(options = {}) {
|
|
999
|
+
const memoryStorage = (0, import_utils2.createMemoryStorage)();
|
|
1000
|
+
return new ChallengeService({
|
|
1001
|
+
...options,
|
|
1002
|
+
storageInstance: memoryStorage
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// plugins/plugin-approval/src/approval.symbols.ts
|
|
1007
|
+
var ApprovalStoreToken = /* @__PURE__ */ Symbol.for(
|
|
1008
|
+
"plugin:approval:store"
|
|
1009
|
+
);
|
|
1010
|
+
var ApprovalServiceToken = /* @__PURE__ */ Symbol.for(
|
|
1011
|
+
"plugin:approval:service"
|
|
1012
|
+
);
|
|
1013
|
+
var ChallengeServiceToken = /* @__PURE__ */ Symbol.for(
|
|
1014
|
+
"plugin:approval:challenge-service"
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
// plugins/plugin-approval/src/hooks/approval-check.hook.ts
|
|
1018
|
+
var import_sdk4 = require("@frontmcp/sdk");
|
|
1019
|
+
var ApprovalCheckPlugin = class extends import_sdk4.DynamicPlugin {
|
|
1020
|
+
async checkApproval(flowCtx) {
|
|
1021
|
+
const { tool, toolContext } = flowCtx.state;
|
|
1022
|
+
if (!tool || !toolContext) return;
|
|
1023
|
+
const metadata = tool.metadata;
|
|
1024
|
+
const approvalConfig = this.resolveApprovalConfig(
|
|
1025
|
+
metadata["approval"]
|
|
1026
|
+
);
|
|
1027
|
+
if (!approvalConfig.required) {
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (approvalConfig.skipApproval) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const ctx = toolContext.tryGetContext?.();
|
|
1034
|
+
const sessionId = ctx?.sessionId ?? "unknown";
|
|
1035
|
+
const userId = this.getStringExtra(ctx?.authInfo?.extra, "userId") ?? this.getStringExtra(ctx?.authInfo?.extra, "sub") ?? ctx?.authInfo?.clientId;
|
|
1036
|
+
const currentContext = this.getCurrentContext(flowCtx);
|
|
1037
|
+
if (this.isPreApprovedContext(approvalConfig, currentContext)) {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const approvalStore = this.get(ApprovalStoreToken);
|
|
1041
|
+
const approval = await approvalStore.getApproval(tool.fullName, sessionId, userId);
|
|
1042
|
+
if (approvalConfig.alwaysPrompt) {
|
|
1043
|
+
await this.handleApprovalRequired(flowCtx, approvalConfig, approval);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (approval?.state === "approved" /* APPROVED */) {
|
|
1047
|
+
if (!this.isExpired(approval)) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (approval?.state === "denied" /* DENIED */) {
|
|
1052
|
+
throw new ApprovalRequiredError({
|
|
1053
|
+
toolId: tool.fullName,
|
|
1054
|
+
state: "denied",
|
|
1055
|
+
message: `Tool "${tool.fullName}" execution denied.`
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
await this.handleApprovalRequired(flowCtx, approvalConfig, approval);
|
|
1059
|
+
}
|
|
1060
|
+
resolveApprovalConfig(config) {
|
|
1061
|
+
if (config === true) {
|
|
1062
|
+
return { required: true, defaultScope: "session" /* SESSION */ };
|
|
1063
|
+
}
|
|
1064
|
+
if (config === false || config === void 0) {
|
|
1065
|
+
return { required: false };
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
...config,
|
|
1069
|
+
required: config.required ?? true,
|
|
1070
|
+
defaultScope: config.defaultScope ?? "session" /* SESSION */
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
isExpired(approval) {
|
|
1074
|
+
if (!approval.expiresAt) return false;
|
|
1075
|
+
return Date.now() > approval.expiresAt;
|
|
1076
|
+
}
|
|
1077
|
+
getCurrentContext(flowCtx) {
|
|
1078
|
+
const { toolContext } = flowCtx.state;
|
|
1079
|
+
const ctx = toolContext?.tryGetContext?.();
|
|
1080
|
+
const inputContext = toolContext?.input?.["context"];
|
|
1081
|
+
const contextFromInput = this.isApprovalContext(inputContext) ? inputContext : void 0;
|
|
1082
|
+
const sessionContext = ctx?.authInfo?.extra?.["approvalContext"];
|
|
1083
|
+
const contextFromSession = this.isApprovalContext(sessionContext) ? sessionContext : void 0;
|
|
1084
|
+
return contextFromInput ?? contextFromSession;
|
|
1085
|
+
}
|
|
1086
|
+
isApprovalContext(value) {
|
|
1087
|
+
return typeof value === "object" && value !== null && "type" in value && "identifier" in value && typeof value.type === "string" && typeof value.identifier === "string";
|
|
1088
|
+
}
|
|
1089
|
+
getStringExtra(extra, key) {
|
|
1090
|
+
if (!extra) return void 0;
|
|
1091
|
+
const value = extra[key];
|
|
1092
|
+
return typeof value === "string" ? value : void 0;
|
|
1093
|
+
}
|
|
1094
|
+
isPreApprovedContext(config, currentContext) {
|
|
1095
|
+
if (!currentContext || !config.preApprovedContexts?.length) {
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
return config.preApprovedContexts.some(
|
|
1099
|
+
(preApproved) => preApproved.type === currentContext.type && preApproved.identifier === currentContext.identifier
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
async handleApprovalRequired(flowCtx, config, existingApproval) {
|
|
1103
|
+
const { tool } = flowCtx.state;
|
|
1104
|
+
const message = config.approvalMessage ?? `Tool "${tool?.fullName}" requires approval to execute. Allow?`;
|
|
1105
|
+
const isExpiredApproval = existingApproval ? this.isExpired(existingApproval) : false;
|
|
1106
|
+
throw new ApprovalRequiredError({
|
|
1107
|
+
toolId: tool?.fullName ?? "unknown",
|
|
1108
|
+
state: isExpiredApproval ? "expired" : "pending",
|
|
1109
|
+
message,
|
|
1110
|
+
approvalOptions: {
|
|
1111
|
+
allowedScopes: config.allowedScopes,
|
|
1112
|
+
defaultScope: config.defaultScope,
|
|
1113
|
+
maxTtlMs: config.maxTtlMs,
|
|
1114
|
+
category: config.category,
|
|
1115
|
+
riskLevel: config.riskLevel
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
__decorateClass([
|
|
1121
|
+
import_sdk4.ToolHook.Will("execute", { priority: 100 })
|
|
1122
|
+
], ApprovalCheckPlugin.prototype, "checkApproval", 1);
|
|
1123
|
+
ApprovalCheckPlugin = __decorateClass([
|
|
1124
|
+
(0, import_sdk4.Plugin)({
|
|
1125
|
+
name: "approval:check",
|
|
1126
|
+
description: "Checks tool approval state before execution"
|
|
1127
|
+
})
|
|
1128
|
+
], ApprovalCheckPlugin);
|
|
1129
|
+
|
|
1130
|
+
// plugins/plugin-approval/src/approval.plugin.ts
|
|
1131
|
+
var ApprovalPlugin = class extends import_sdk5.DynamicPlugin {
|
|
1132
|
+
options;
|
|
1133
|
+
constructor(options = {}) {
|
|
1134
|
+
super();
|
|
1135
|
+
this.options = {
|
|
1136
|
+
...ApprovalPlugin.defaultOptions,
|
|
1137
|
+
...options
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Get plugin metadata including nested plugins.
|
|
1142
|
+
*/
|
|
1143
|
+
static getPluginMetadata(_options) {
|
|
1144
|
+
return { plugins: [ApprovalCheckPlugin] };
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
__publicField(ApprovalPlugin, "defaultOptions", {
|
|
1148
|
+
namespace: "approval",
|
|
1149
|
+
mode: "recheck",
|
|
1150
|
+
enableAudit: true,
|
|
1151
|
+
maxDelegationDepth: 3,
|
|
1152
|
+
cleanupIntervalSeconds: 60
|
|
1153
|
+
});
|
|
1154
|
+
/**
|
|
1155
|
+
* Dynamic providers based on plugin options.
|
|
1156
|
+
*/
|
|
1157
|
+
__publicField(ApprovalPlugin, "dynamicProviders", (options) => {
|
|
1158
|
+
const providers = [];
|
|
1159
|
+
const config = {
|
|
1160
|
+
...ApprovalPlugin.defaultOptions,
|
|
1161
|
+
...options
|
|
1162
|
+
};
|
|
1163
|
+
providers.push({
|
|
1164
|
+
name: "approval:store",
|
|
1165
|
+
provide: ApprovalStoreToken,
|
|
1166
|
+
inject: () => [],
|
|
1167
|
+
useFactory: async () => {
|
|
1168
|
+
const store = new ApprovalStorageStore({
|
|
1169
|
+
storage: config.storage,
|
|
1170
|
+
storageInstance: config.storageInstance,
|
|
1171
|
+
namespace: config.namespace,
|
|
1172
|
+
cleanupIntervalSeconds: config.cleanupIntervalSeconds
|
|
1173
|
+
});
|
|
1174
|
+
await store.initialize();
|
|
1175
|
+
return store;
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
if (config.mode === "webhook") {
|
|
1179
|
+
providers.push({
|
|
1180
|
+
name: "approval:challenge-service",
|
|
1181
|
+
provide: ChallengeServiceToken,
|
|
1182
|
+
inject: () => [],
|
|
1183
|
+
useFactory: async () => {
|
|
1184
|
+
const service = new ChallengeService({
|
|
1185
|
+
storage: config.storage,
|
|
1186
|
+
storageInstance: config.storageInstance,
|
|
1187
|
+
namespace: `${config.namespace}:challenge`,
|
|
1188
|
+
defaultTtlSeconds: config.webhook?.challengeTtl ?? 300
|
|
1189
|
+
});
|
|
1190
|
+
await service.initialize();
|
|
1191
|
+
return service;
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
providers.push({
|
|
1196
|
+
name: "approval:service",
|
|
1197
|
+
provide: ApprovalServiceToken,
|
|
1198
|
+
scope: import_sdk5.ProviderScope.CONTEXT,
|
|
1199
|
+
inject: () => [ApprovalStoreToken, import_sdk5.FRONTMCP_CONTEXT],
|
|
1200
|
+
useFactory: (store, ctx) => {
|
|
1201
|
+
const userId = ctx.authInfo?.extra?.["userId"] ?? ctx.authInfo?.extra?.["sub"] ?? ctx.authInfo?.clientId;
|
|
1202
|
+
return createApprovalService(store, ctx.sessionId, userId);
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
return providers;
|
|
1206
|
+
});
|
|
1207
|
+
ApprovalPlugin = __decorateClass([
|
|
1208
|
+
(0, import_sdk5.Plugin)({
|
|
1209
|
+
name: "approval",
|
|
1210
|
+
description: "Tool authorization workflow with PKCE webhook security",
|
|
1211
|
+
contextExtensions: [
|
|
1212
|
+
{
|
|
1213
|
+
property: "approval",
|
|
1214
|
+
token: ApprovalServiceToken,
|
|
1215
|
+
errorMessage: "ApprovalPlugin is not installed. Add ApprovalPlugin.init() to your plugins array."
|
|
1216
|
+
}
|
|
1217
|
+
]
|
|
1218
|
+
})
|
|
1219
|
+
], ApprovalPlugin);
|
|
1220
|
+
|
|
1221
|
+
// plugins/plugin-approval/src/approval.context-extension.ts
|
|
1222
|
+
var installed = false;
|
|
1223
|
+
function installApprovalContextExtension() {
|
|
1224
|
+
if (installed) return;
|
|
1225
|
+
const { ExecutionContextBase } = require("@frontmcp/sdk");
|
|
1226
|
+
Object.defineProperty(ExecutionContextBase.prototype, "approval", {
|
|
1227
|
+
get: function() {
|
|
1228
|
+
return this.get(ApprovalServiceToken);
|
|
1229
|
+
},
|
|
1230
|
+
configurable: true,
|
|
1231
|
+
enumerable: false
|
|
1232
|
+
});
|
|
1233
|
+
installed = true;
|
|
1234
|
+
}
|
|
1235
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1236
|
+
0 && (module.exports = {
|
|
1237
|
+
ApprovalCheckPlugin,
|
|
1238
|
+
ApprovalError,
|
|
1239
|
+
ApprovalExpiredError,
|
|
1240
|
+
ApprovalOperationError,
|
|
1241
|
+
ApprovalPlugin,
|
|
1242
|
+
ApprovalPluginClass,
|
|
1243
|
+
ApprovalRequiredError,
|
|
1244
|
+
ApprovalScope,
|
|
1245
|
+
ApprovalScopeNotAllowedError,
|
|
1246
|
+
ApprovalService,
|
|
1247
|
+
ApprovalServiceToken,
|
|
1248
|
+
ApprovalState,
|
|
1249
|
+
ApprovalStorageStore,
|
|
1250
|
+
ApprovalStoreToken,
|
|
1251
|
+
ChallengeService,
|
|
1252
|
+
ChallengeServiceToken,
|
|
1253
|
+
ChallengeValidationError,
|
|
1254
|
+
adminGrantor,
|
|
1255
|
+
adminRevoker,
|
|
1256
|
+
agentGrantor,
|
|
1257
|
+
apiGrantor,
|
|
1258
|
+
createApprovalMemoryStore,
|
|
1259
|
+
createApprovalService,
|
|
1260
|
+
createMemoryChallengeService,
|
|
1261
|
+
customGrantor,
|
|
1262
|
+
expiryRevoker,
|
|
1263
|
+
installApprovalContextExtension,
|
|
1264
|
+
isApiGrantor,
|
|
1265
|
+
isAutoGrantor,
|
|
1266
|
+
isDelegatedGrantor,
|
|
1267
|
+
isGrantorSource,
|
|
1268
|
+
isHumanGrantor,
|
|
1269
|
+
normalizeGrantor,
|
|
1270
|
+
normalizeRevoker,
|
|
1271
|
+
oauthGrantor,
|
|
1272
|
+
policyGrantor,
|
|
1273
|
+
policyRevoker,
|
|
1274
|
+
sessionEndRevoker,
|
|
1275
|
+
systemGrantor,
|
|
1276
|
+
testGrantor,
|
|
1277
|
+
userGrantor,
|
|
1278
|
+
userRevoker
|
|
1279
|
+
});
|