@frontmcp/plugin-approval 1.8.1 → 1.8.3
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/policy.d.ts +30 -0
- package/approval/policy.d.ts.map +1 -0
- package/approval.identity.d.ts +3 -5
- package/approval.identity.d.ts.map +1 -1
- package/approval.plugin.d.ts.map +1 -1
- package/esm/index.mjs +375 -239
- package/esm/package.json +4 -4
- package/hooks/approval-check.hook.d.ts +9 -2
- package/hooks/approval-check.hook.d.ts.map +1 -1
- package/index.js +389 -254
- package/package.json +4 -4
- package/services/approval.service.d.ts +17 -6
- package/services/approval.service.d.ts.map +1 -1
- package/stores/approval-storage.store.d.ts +30 -3
- package/stores/approval-storage.store.d.ts.map +1 -1
- package/stores/approval-store.interface.d.ts +11 -3
- package/stores/approval-store.interface.d.ts.map +1 -1
package/index.js
CHANGED
|
@@ -76,10 +76,208 @@ __export(index_exports, {
|
|
|
76
76
|
module.exports = __toCommonJS(index_exports);
|
|
77
77
|
|
|
78
78
|
// plugins/plugin-approval/src/approval.plugin.ts
|
|
79
|
-
var
|
|
79
|
+
var import_sdk5 = require("@frontmcp/sdk");
|
|
80
|
+
|
|
81
|
+
// plugins/plugin-approval/src/approval/errors.ts
|
|
82
|
+
var ApprovalError = class extends Error {
|
|
83
|
+
constructor(message) {
|
|
84
|
+
super(message);
|
|
85
|
+
this.name = "ApprovalError";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var ApprovalRequiredError = class extends ApprovalError {
|
|
89
|
+
constructor(details) {
|
|
90
|
+
super(details.message);
|
|
91
|
+
this.details = details;
|
|
92
|
+
this.name = "ApprovalRequiredError";
|
|
93
|
+
}
|
|
94
|
+
details;
|
|
95
|
+
/**
|
|
96
|
+
* Convert to a JSON-RPC compatible error structure.
|
|
97
|
+
*/
|
|
98
|
+
toJsonRpcError() {
|
|
99
|
+
return {
|
|
100
|
+
code: -32600,
|
|
101
|
+
// Invalid Request
|
|
102
|
+
message: this.details.message,
|
|
103
|
+
data: {
|
|
104
|
+
type: "approval_required",
|
|
105
|
+
toolId: this.details.toolId,
|
|
106
|
+
state: this.details.state,
|
|
107
|
+
options: this.details.approvalOptions
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var ApprovalOperationError = class extends ApprovalError {
|
|
113
|
+
constructor(operation, reason) {
|
|
114
|
+
super(`Approval ${operation} failed: ${reason}`);
|
|
115
|
+
this.operation = operation;
|
|
116
|
+
this.reason = reason;
|
|
117
|
+
this.name = "ApprovalOperationError";
|
|
118
|
+
}
|
|
119
|
+
operation;
|
|
120
|
+
reason;
|
|
121
|
+
/**
|
|
122
|
+
* Convert to a JSON-RPC compatible error structure.
|
|
123
|
+
*/
|
|
124
|
+
toJsonRpcError() {
|
|
125
|
+
return {
|
|
126
|
+
code: -32603,
|
|
127
|
+
// Internal Error
|
|
128
|
+
message: "Approval operation failed",
|
|
129
|
+
data: {
|
|
130
|
+
type: "approval_operation_error",
|
|
131
|
+
operation: this.operation
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
var ApprovalScopeNotAllowedError = class extends ApprovalError {
|
|
137
|
+
constructor(requestedScope, allowedScopes) {
|
|
138
|
+
super(
|
|
139
|
+
`Approval scope '${requestedScope}' is not allowed for this tool. Allowed scopes: ${allowedScopes.join(", ")}`
|
|
140
|
+
);
|
|
141
|
+
this.requestedScope = requestedScope;
|
|
142
|
+
this.allowedScopes = allowedScopes;
|
|
143
|
+
this.name = "ApprovalScopeNotAllowedError";
|
|
144
|
+
}
|
|
145
|
+
requestedScope;
|
|
146
|
+
allowedScopes;
|
|
147
|
+
/**
|
|
148
|
+
* Convert to a JSON-RPC compatible error structure.
|
|
149
|
+
*/
|
|
150
|
+
toJsonRpcError() {
|
|
151
|
+
return {
|
|
152
|
+
code: -32602,
|
|
153
|
+
// Invalid Params
|
|
154
|
+
message: this.message,
|
|
155
|
+
data: {
|
|
156
|
+
type: "approval_scope_not_allowed",
|
|
157
|
+
requestedScope: this.requestedScope,
|
|
158
|
+
allowedScopes: this.allowedScopes
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
var ApprovalExpiredError = class extends ApprovalError {
|
|
164
|
+
constructor(toolId, expiredAt) {
|
|
165
|
+
super(`Approval for tool '${toolId}' expired at ${new Date(expiredAt).toISOString()}`);
|
|
166
|
+
this.toolId = toolId;
|
|
167
|
+
this.expiredAt = expiredAt;
|
|
168
|
+
this.name = "ApprovalExpiredError";
|
|
169
|
+
}
|
|
170
|
+
toolId;
|
|
171
|
+
expiredAt;
|
|
172
|
+
/**
|
|
173
|
+
* Convert to a JSON-RPC compatible error structure.
|
|
174
|
+
*/
|
|
175
|
+
toJsonRpcError() {
|
|
176
|
+
return {
|
|
177
|
+
code: -32600,
|
|
178
|
+
// Invalid Request
|
|
179
|
+
message: this.message,
|
|
180
|
+
data: {
|
|
181
|
+
type: "approval_expired",
|
|
182
|
+
toolId: this.toolId,
|
|
183
|
+
expiredAt: this.expiredAt
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var ChallengeValidationError = class extends ApprovalError {
|
|
189
|
+
constructor(reason = "invalid", message) {
|
|
190
|
+
super(message ?? `PKCE challenge validation failed: ${reason}`);
|
|
191
|
+
this.reason = reason;
|
|
192
|
+
this.name = "ChallengeValidationError";
|
|
193
|
+
}
|
|
194
|
+
reason;
|
|
195
|
+
/**
|
|
196
|
+
* Convert to a JSON-RPC compatible error structure.
|
|
197
|
+
*/
|
|
198
|
+
toJsonRpcError() {
|
|
199
|
+
return {
|
|
200
|
+
code: -32600,
|
|
201
|
+
// Invalid Request
|
|
202
|
+
message: this.message,
|
|
203
|
+
data: {
|
|
204
|
+
type: "challenge_validation_error",
|
|
205
|
+
reason: this.reason
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// plugins/plugin-approval/src/approval/types.ts
|
|
212
|
+
var ApprovalScope = /* @__PURE__ */ ((ApprovalScope2) => {
|
|
213
|
+
ApprovalScope2["SESSION"] = "session";
|
|
214
|
+
ApprovalScope2["USER"] = "user";
|
|
215
|
+
ApprovalScope2["TIME_LIMITED"] = "time_limited";
|
|
216
|
+
ApprovalScope2["TOOL_SPECIFIC"] = "tool_specific";
|
|
217
|
+
ApprovalScope2["CONTEXT_SPECIFIC"] = "context_specific";
|
|
218
|
+
return ApprovalScope2;
|
|
219
|
+
})(ApprovalScope || {});
|
|
220
|
+
var ApprovalState = /* @__PURE__ */ ((ApprovalState2) => {
|
|
221
|
+
ApprovalState2["PENDING"] = "pending";
|
|
222
|
+
ApprovalState2["APPROVED"] = "approved";
|
|
223
|
+
ApprovalState2["DENIED"] = "denied";
|
|
224
|
+
ApprovalState2["EXPIRED"] = "expired";
|
|
225
|
+
return ApprovalState2;
|
|
226
|
+
})(ApprovalState || {});
|
|
227
|
+
|
|
228
|
+
// plugins/plugin-approval/src/approval/policy.ts
|
|
229
|
+
function resolveApprovalRequirement(config) {
|
|
230
|
+
if (config === true) {
|
|
231
|
+
return { required: true, defaultScope: "session" /* SESSION */ };
|
|
232
|
+
}
|
|
233
|
+
if (config === false || config === void 0) {
|
|
234
|
+
return { required: false };
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
...config,
|
|
238
|
+
required: config.required ?? true,
|
|
239
|
+
defaultScope: config.defaultScope ?? "session" /* SESSION */
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function effectiveExpiresAt(record, requirement) {
|
|
243
|
+
const capped = requirement.maxTtlMs !== void 0 ? record.grantedAt + requirement.maxTtlMs : void 0;
|
|
244
|
+
if (record.expiresAt === void 0) return capped;
|
|
245
|
+
return capped === void 0 ? record.expiresAt : Math.min(record.expiresAt, capped);
|
|
246
|
+
}
|
|
247
|
+
function isApprovalExpired(record, requirement, now) {
|
|
248
|
+
const expiresAt = effectiveExpiresAt(record, requirement);
|
|
249
|
+
return expiresAt !== void 0 && now > expiresAt;
|
|
250
|
+
}
|
|
251
|
+
function isApprovalUsable(record, requirement, now) {
|
|
252
|
+
if (record.state !== "approved" /* APPROVED */) return false;
|
|
253
|
+
if (requirement.allowedScopes?.length && !requirement.allowedScopes.includes(record.scope)) return false;
|
|
254
|
+
return !isApprovalExpired(record, requirement, now);
|
|
255
|
+
}
|
|
256
|
+
function assertValidTtl(ttlMs) {
|
|
257
|
+
if (ttlMs === void 0) return;
|
|
258
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
259
|
+
throw new ApprovalOperationError("grant", `ttlMs must be a positive number of milliseconds, got ${ttlMs}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function checkGrantAgainstPolicy(toolId, scope, ttlMs, requirement) {
|
|
263
|
+
assertValidTtl(ttlMs);
|
|
264
|
+
if (!requirement) return ttlMs;
|
|
265
|
+
if (requirement.allowedScopes?.length && !requirement.allowedScopes.includes(scope)) {
|
|
266
|
+
throw new ApprovalScopeNotAllowedError(scope, requirement.allowedScopes);
|
|
267
|
+
}
|
|
268
|
+
const { maxTtlMs } = requirement;
|
|
269
|
+
if (maxTtlMs === void 0) return ttlMs;
|
|
270
|
+
if (ttlMs === void 0) return maxTtlMs;
|
|
271
|
+
if (ttlMs > maxTtlMs) {
|
|
272
|
+
throw new ApprovalOperationError(
|
|
273
|
+
"grant",
|
|
274
|
+
`ttlMs ${ttlMs} exceeds the maximum of ${maxTtlMs} ms that tool "${toolId}" allows`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return ttlMs;
|
|
278
|
+
}
|
|
80
279
|
|
|
81
280
|
// plugins/plugin-approval/src/approval.identity.ts
|
|
82
|
-
var import_sdk = require("@frontmcp/sdk");
|
|
83
281
|
var import_utils = require("@frontmcp/utils");
|
|
84
282
|
function nonEmptyString(value) {
|
|
85
283
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -87,8 +285,8 @@ function nonEmptyString(value) {
|
|
|
87
285
|
function resolveApprovalIdentity(ctx) {
|
|
88
286
|
const extra = ctx?.authInfo?.extra;
|
|
89
287
|
const userId = nonEmptyString(extra?.["userId"]) ?? nonEmptyString(extra?.["sub"]) ?? nonEmptyString(ctx?.authInfo?.clientId);
|
|
90
|
-
const sessionId = nonEmptyString(ctx?.
|
|
91
|
-
if (sessionId
|
|
288
|
+
const sessionId = nonEmptyString(ctx?.verifiedSessionId);
|
|
289
|
+
if (sessionId) {
|
|
92
290
|
return { sessionId, userId };
|
|
93
291
|
}
|
|
94
292
|
return { sessionId: userId ? `stateless-user:${userId}` : `unidentified:${(0, import_utils.randomUUID)()}`, userId };
|
|
@@ -106,24 +304,7 @@ var ChallengeServiceToken = /* @__PURE__ */ Symbol.for(
|
|
|
106
304
|
);
|
|
107
305
|
|
|
108
306
|
// plugins/plugin-approval/src/hooks/approval-check.hook.ts
|
|
109
|
-
var
|
|
110
|
-
|
|
111
|
-
// plugins/plugin-approval/src/approval/types.ts
|
|
112
|
-
var ApprovalScope = /* @__PURE__ */ ((ApprovalScope2) => {
|
|
113
|
-
ApprovalScope2["SESSION"] = "session";
|
|
114
|
-
ApprovalScope2["USER"] = "user";
|
|
115
|
-
ApprovalScope2["TIME_LIMITED"] = "time_limited";
|
|
116
|
-
ApprovalScope2["TOOL_SPECIFIC"] = "tool_specific";
|
|
117
|
-
ApprovalScope2["CONTEXT_SPECIFIC"] = "context_specific";
|
|
118
|
-
return ApprovalScope2;
|
|
119
|
-
})(ApprovalScope || {});
|
|
120
|
-
var ApprovalState = /* @__PURE__ */ ((ApprovalState2) => {
|
|
121
|
-
ApprovalState2["PENDING"] = "pending";
|
|
122
|
-
ApprovalState2["APPROVED"] = "approved";
|
|
123
|
-
ApprovalState2["DENIED"] = "denied";
|
|
124
|
-
ApprovalState2["EXPIRED"] = "expired";
|
|
125
|
-
return ApprovalState2;
|
|
126
|
-
})(ApprovalState || {});
|
|
307
|
+
var import_sdk = require("@frontmcp/sdk");
|
|
127
308
|
|
|
128
309
|
// plugins/plugin-approval/src/approval/schemas.ts
|
|
129
310
|
var import_lazy_zod = require("@frontmcp/lazy-zod");
|
|
@@ -341,144 +522,14 @@ function isApiGrantor(grantor) {
|
|
|
341
522
|
return grantor.source === "api" || grantor.source === "oauth";
|
|
342
523
|
}
|
|
343
524
|
|
|
344
|
-
// plugins/plugin-approval/src/approval/errors.ts
|
|
345
|
-
var ApprovalError = class extends Error {
|
|
346
|
-
constructor(message) {
|
|
347
|
-
super(message);
|
|
348
|
-
this.name = "ApprovalError";
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
var ApprovalRequiredError = class extends ApprovalError {
|
|
352
|
-
constructor(details) {
|
|
353
|
-
super(details.message);
|
|
354
|
-
this.details = details;
|
|
355
|
-
this.name = "ApprovalRequiredError";
|
|
356
|
-
}
|
|
357
|
-
details;
|
|
358
|
-
/**
|
|
359
|
-
* Convert to a JSON-RPC compatible error structure.
|
|
360
|
-
*/
|
|
361
|
-
toJsonRpcError() {
|
|
362
|
-
return {
|
|
363
|
-
code: -32600,
|
|
364
|
-
// Invalid Request
|
|
365
|
-
message: this.details.message,
|
|
366
|
-
data: {
|
|
367
|
-
type: "approval_required",
|
|
368
|
-
toolId: this.details.toolId,
|
|
369
|
-
state: this.details.state,
|
|
370
|
-
options: this.details.approvalOptions
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
var ApprovalOperationError = class extends ApprovalError {
|
|
376
|
-
constructor(operation, reason) {
|
|
377
|
-
super(`Approval ${operation} failed: ${reason}`);
|
|
378
|
-
this.operation = operation;
|
|
379
|
-
this.reason = reason;
|
|
380
|
-
this.name = "ApprovalOperationError";
|
|
381
|
-
}
|
|
382
|
-
operation;
|
|
383
|
-
reason;
|
|
384
|
-
/**
|
|
385
|
-
* Convert to a JSON-RPC compatible error structure.
|
|
386
|
-
*/
|
|
387
|
-
toJsonRpcError() {
|
|
388
|
-
return {
|
|
389
|
-
code: -32603,
|
|
390
|
-
// Internal Error
|
|
391
|
-
message: "Approval operation failed",
|
|
392
|
-
data: {
|
|
393
|
-
type: "approval_operation_error",
|
|
394
|
-
operation: this.operation
|
|
395
|
-
}
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
var ApprovalScopeNotAllowedError = class extends ApprovalError {
|
|
400
|
-
constructor(requestedScope, allowedScopes) {
|
|
401
|
-
super(
|
|
402
|
-
`Approval scope '${requestedScope}' is not allowed for this tool. Allowed scopes: ${allowedScopes.join(", ")}`
|
|
403
|
-
);
|
|
404
|
-
this.requestedScope = requestedScope;
|
|
405
|
-
this.allowedScopes = allowedScopes;
|
|
406
|
-
this.name = "ApprovalScopeNotAllowedError";
|
|
407
|
-
}
|
|
408
|
-
requestedScope;
|
|
409
|
-
allowedScopes;
|
|
410
|
-
/**
|
|
411
|
-
* Convert to a JSON-RPC compatible error structure.
|
|
412
|
-
*/
|
|
413
|
-
toJsonRpcError() {
|
|
414
|
-
return {
|
|
415
|
-
code: -32602,
|
|
416
|
-
// Invalid Params
|
|
417
|
-
message: this.message,
|
|
418
|
-
data: {
|
|
419
|
-
type: "approval_scope_not_allowed",
|
|
420
|
-
requestedScope: this.requestedScope,
|
|
421
|
-
allowedScopes: this.allowedScopes
|
|
422
|
-
}
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
};
|
|
426
|
-
var ApprovalExpiredError = class extends ApprovalError {
|
|
427
|
-
constructor(toolId, expiredAt) {
|
|
428
|
-
super(`Approval for tool '${toolId}' expired at ${new Date(expiredAt).toISOString()}`);
|
|
429
|
-
this.toolId = toolId;
|
|
430
|
-
this.expiredAt = expiredAt;
|
|
431
|
-
this.name = "ApprovalExpiredError";
|
|
432
|
-
}
|
|
433
|
-
toolId;
|
|
434
|
-
expiredAt;
|
|
435
|
-
/**
|
|
436
|
-
* Convert to a JSON-RPC compatible error structure.
|
|
437
|
-
*/
|
|
438
|
-
toJsonRpcError() {
|
|
439
|
-
return {
|
|
440
|
-
code: -32600,
|
|
441
|
-
// Invalid Request
|
|
442
|
-
message: this.message,
|
|
443
|
-
data: {
|
|
444
|
-
type: "approval_expired",
|
|
445
|
-
toolId: this.toolId,
|
|
446
|
-
expiredAt: this.expiredAt
|
|
447
|
-
}
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
var ChallengeValidationError = class extends ApprovalError {
|
|
452
|
-
constructor(reason = "invalid", message) {
|
|
453
|
-
super(message ?? `PKCE challenge validation failed: ${reason}`);
|
|
454
|
-
this.reason = reason;
|
|
455
|
-
this.name = "ChallengeValidationError";
|
|
456
|
-
}
|
|
457
|
-
reason;
|
|
458
|
-
/**
|
|
459
|
-
* Convert to a JSON-RPC compatible error structure.
|
|
460
|
-
*/
|
|
461
|
-
toJsonRpcError() {
|
|
462
|
-
return {
|
|
463
|
-
code: -32600,
|
|
464
|
-
// Invalid Request
|
|
465
|
-
message: this.message,
|
|
466
|
-
data: {
|
|
467
|
-
type: "challenge_validation_error",
|
|
468
|
-
reason: this.reason
|
|
469
|
-
}
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
|
|
474
525
|
// plugins/plugin-approval/src/hooks/approval-check.hook.ts
|
|
475
526
|
var passedApprovalStores = /* @__PURE__ */ new WeakMap();
|
|
476
|
-
var ApprovalCheckPlugin = class extends
|
|
527
|
+
var ApprovalCheckPlugin = class extends import_sdk.DynamicPlugin {
|
|
477
528
|
async checkApproval(flowCtx) {
|
|
478
529
|
const { tool, toolContext } = flowCtx.state;
|
|
479
530
|
if (!tool || !toolContext) return;
|
|
480
531
|
const metadata = tool.metadata;
|
|
481
|
-
const approvalConfig =
|
|
532
|
+
const approvalConfig = resolveApprovalRequirement(
|
|
482
533
|
metadata["approval"]
|
|
483
534
|
);
|
|
484
535
|
if (!approvalConfig.required) {
|
|
@@ -496,44 +547,43 @@ var ApprovalCheckPlugin = class extends import_sdk2.DynamicPlugin {
|
|
|
496
547
|
}
|
|
497
548
|
async enforceApproval(flowCtx, tool, toolContext, approvalConfig, approvalStore) {
|
|
498
549
|
const { sessionId, userId } = resolveApprovalIdentity(toolContext.tryGetContext?.());
|
|
499
|
-
const
|
|
500
|
-
|
|
550
|
+
const currentContext = this.getCurrentContext(flowCtx);
|
|
551
|
+
const records = await this.readApprovals(approvalStore, tool.fullName, sessionId, userId, currentContext);
|
|
552
|
+
if (records.some((record) => record.state === "denied" /* DENIED */)) {
|
|
501
553
|
throw new ApprovalRequiredError({
|
|
502
554
|
toolId: tool.fullName,
|
|
503
555
|
state: "denied",
|
|
504
556
|
message: `Tool "${tool.fullName}" execution denied.`
|
|
505
557
|
});
|
|
506
558
|
}
|
|
507
|
-
if (this.isPreApprovedContext(approvalConfig,
|
|
559
|
+
if (this.isPreApprovedContext(approvalConfig, currentContext)) {
|
|
508
560
|
return;
|
|
509
561
|
}
|
|
562
|
+
const approved = records.filter((record) => record.state === "approved" /* APPROVED */);
|
|
510
563
|
if (approvalConfig.alwaysPrompt) {
|
|
511
|
-
await this.handleApprovalRequired(flowCtx, approvalConfig,
|
|
564
|
+
await this.handleApprovalRequired(flowCtx, approvalConfig, approved[0]);
|
|
512
565
|
return;
|
|
513
566
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
567
|
+
const now = Date.now();
|
|
568
|
+
if (approved.some((record) => isApprovalUsable(record, approvalConfig, now))) {
|
|
569
|
+
return;
|
|
518
570
|
}
|
|
519
|
-
await this.handleApprovalRequired(
|
|
571
|
+
await this.handleApprovalRequired(
|
|
572
|
+
flowCtx,
|
|
573
|
+
approvalConfig,
|
|
574
|
+
approved.find((record) => isApprovalExpired(record, approvalConfig, now)) ?? approved[0]
|
|
575
|
+
);
|
|
520
576
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
577
|
+
/**
|
|
578
|
+
* The caller's approvals and denials of the tool, from the context it runs in too. Stores
|
|
579
|
+
* without `getApprovals()` give the one record `getApproval()` picks.
|
|
580
|
+
*/
|
|
581
|
+
async readApprovals(store, toolId, sessionId, userId, context) {
|
|
582
|
+
if (store.getApprovals) {
|
|
583
|
+
return store.getApprovals(toolId, sessionId, userId, context);
|
|
527
584
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
required: config.required ?? true,
|
|
531
|
-
defaultScope: config.defaultScope ?? "session" /* SESSION */
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
isExpired(approval) {
|
|
535
|
-
if (!approval.expiresAt) return false;
|
|
536
|
-
return Date.now() > approval.expiresAt;
|
|
585
|
+
const record = await store.getApproval(toolId, sessionId, userId, context);
|
|
586
|
+
return record ? [record] : [];
|
|
537
587
|
}
|
|
538
588
|
/**
|
|
539
589
|
* The context this call is running in, as established by the SESSION (GHSA-r848-p7wf-96rc).
|
|
@@ -563,7 +613,7 @@ var ApprovalCheckPlugin = class extends import_sdk2.DynamicPlugin {
|
|
|
563
613
|
async handleApprovalRequired(flowCtx, config, existingApproval) {
|
|
564
614
|
const { tool } = flowCtx.state;
|
|
565
615
|
const message = config.approvalMessage ?? `Tool "${tool?.fullName}" requires approval to execute. Allow?`;
|
|
566
|
-
const isExpiredApproval = existingApproval ?
|
|
616
|
+
const isExpiredApproval = existingApproval ? isApprovalExpired(existingApproval, config, Date.now()) : false;
|
|
567
617
|
throw new ApprovalRequiredError({
|
|
568
618
|
toolId: tool?.fullName ?? "unknown",
|
|
569
619
|
state: isExpiredApproval ? "expired" : "pending",
|
|
@@ -579,26 +629,34 @@ var ApprovalCheckPlugin = class extends import_sdk2.DynamicPlugin {
|
|
|
579
629
|
}
|
|
580
630
|
};
|
|
581
631
|
__decorateClass([
|
|
582
|
-
|
|
632
|
+
import_sdk.ToolHook.Will("execute", { priority: 100, appliesTo: "uncovered-apps" })
|
|
583
633
|
], ApprovalCheckPlugin.prototype, "checkApproval", 1);
|
|
584
634
|
ApprovalCheckPlugin = __decorateClass([
|
|
585
|
-
(0,
|
|
635
|
+
(0, import_sdk.Plugin)({
|
|
586
636
|
name: "approval:check",
|
|
587
|
-
description: "Checks tool approval state before execution"
|
|
637
|
+
description: "Checks tool approval state before execution",
|
|
638
|
+
// A server where a tool or agent declares `approval` and this gate does not reach it refuses to start.
|
|
639
|
+
enforcesMetadata: ["approval"]
|
|
588
640
|
})
|
|
589
641
|
], ApprovalCheckPlugin);
|
|
590
642
|
|
|
591
643
|
// plugins/plugin-approval/src/services/approval.service.ts
|
|
592
|
-
var
|
|
644
|
+
var import_sdk2 = require("@frontmcp/sdk");
|
|
593
645
|
var ApprovalService = class {
|
|
594
|
-
constructor(store, sessionId, userId) {
|
|
646
|
+
constructor(store, sessionId, userId, requirementOf = () => void 0) {
|
|
595
647
|
this.store = store;
|
|
596
648
|
this.sessionId = sessionId;
|
|
597
649
|
this.userId = userId;
|
|
650
|
+
this.requirementOf = requirementOf;
|
|
598
651
|
}
|
|
599
652
|
store;
|
|
600
653
|
sessionId;
|
|
601
654
|
userId;
|
|
655
|
+
requirementOf;
|
|
656
|
+
/** Checks a grant against the tool's `allowedScopes` and `maxTtlMs`; returns the ttl to store. */
|
|
657
|
+
checkGrant(toolId, scope, ttlMs) {
|
|
658
|
+
return checkGrantAgainstPolicy(toolId, scope, ttlMs, this.requirementOf(toolId));
|
|
659
|
+
}
|
|
602
660
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
603
661
|
// Query Methods
|
|
604
662
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -606,7 +664,14 @@ var ApprovalService = class {
|
|
|
606
664
|
* Check if a tool is approved for current session/user.
|
|
607
665
|
*/
|
|
608
666
|
async isApproved(toolId, context) {
|
|
609
|
-
|
|
667
|
+
const requirement = this.requirementOf(toolId);
|
|
668
|
+
if (!requirement || !this.store.getApprovals) {
|
|
669
|
+
return this.store.isApproved(toolId, this.sessionId, this.userId, context);
|
|
670
|
+
}
|
|
671
|
+
const records = await this.store.getApprovals(toolId, this.sessionId, this.userId, context);
|
|
672
|
+
if (records.some((record) => record.state === "denied" /* DENIED */)) return false;
|
|
673
|
+
const now = Date.now();
|
|
674
|
+
return records.some((record) => isApprovalUsable(record, requirement, now));
|
|
610
675
|
}
|
|
611
676
|
/**
|
|
612
677
|
* Get approval record for a tool.
|
|
@@ -637,14 +702,15 @@ var ApprovalService = class {
|
|
|
637
702
|
});
|
|
638
703
|
}
|
|
639
704
|
/**
|
|
640
|
-
* Query approvals with custom
|
|
705
|
+
* Query the current caller's approvals (those of its session and those of its user) with custom
|
|
706
|
+
* filters. A `sessionId` or `userId` in the query narrows the result further; it never reaches
|
|
707
|
+
* another caller's records. Read the store directly for administrative queries.
|
|
641
708
|
*/
|
|
642
709
|
async queryApprovals(query) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
sessionId
|
|
646
|
-
|
|
647
|
-
});
|
|
710
|
+
const records = await this.store.queryApprovals(query);
|
|
711
|
+
return records.filter(
|
|
712
|
+
(record) => record.sessionId === this.sessionId || this.userId !== void 0 && record.userId === this.userId
|
|
713
|
+
);
|
|
648
714
|
}
|
|
649
715
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
650
716
|
// Grant Methods
|
|
@@ -653,9 +719,11 @@ var ApprovalService = class {
|
|
|
653
719
|
* Grant session-scoped approval for a tool.
|
|
654
720
|
*/
|
|
655
721
|
async grantSessionApproval(toolId, options = {}) {
|
|
722
|
+
const ttlMs = this.checkGrant(toolId, "session" /* SESSION */);
|
|
656
723
|
return this.store.grantApproval({
|
|
657
724
|
toolId,
|
|
658
725
|
scope: "session" /* SESSION */,
|
|
726
|
+
ttlMs,
|
|
659
727
|
sessionId: this.sessionId,
|
|
660
728
|
grantedBy: options.grantedBy ?? "policy",
|
|
661
729
|
reason: options.reason,
|
|
@@ -669,9 +737,11 @@ var ApprovalService = class {
|
|
|
669
737
|
if (!this.userId) {
|
|
670
738
|
throw new Error("Cannot grant user approval without userId");
|
|
671
739
|
}
|
|
740
|
+
const ttlMs = this.checkGrant(toolId, "user" /* USER */);
|
|
672
741
|
return this.store.grantApproval({
|
|
673
742
|
toolId,
|
|
674
743
|
scope: "user" /* USER */,
|
|
744
|
+
ttlMs,
|
|
675
745
|
userId: this.userId,
|
|
676
746
|
grantedBy: options.grantedBy ?? "policy",
|
|
677
747
|
reason: options.reason,
|
|
@@ -682,6 +752,7 @@ var ApprovalService = class {
|
|
|
682
752
|
* Grant time-limited approval for a tool.
|
|
683
753
|
*/
|
|
684
754
|
async grantTimeLimitedApproval(toolId, ttlMs, options = {}) {
|
|
755
|
+
this.checkGrant(toolId, "time_limited" /* TIME_LIMITED */, ttlMs);
|
|
685
756
|
return this.store.grantApproval({
|
|
686
757
|
toolId,
|
|
687
758
|
scope: "time_limited" /* TIME_LIMITED */,
|
|
@@ -697,9 +768,11 @@ var ApprovalService = class {
|
|
|
697
768
|
* Grant context-specific approval for a tool.
|
|
698
769
|
*/
|
|
699
770
|
async grantContextApproval(toolId, context, options = {}) {
|
|
771
|
+
const ttlMs = this.checkGrant(toolId, "context_specific" /* CONTEXT_SPECIFIC */);
|
|
700
772
|
return this.store.grantApproval({
|
|
701
773
|
toolId,
|
|
702
774
|
scope: "context_specific" /* CONTEXT_SPECIFIC */,
|
|
775
|
+
ttlMs,
|
|
703
776
|
context,
|
|
704
777
|
sessionId: this.sessionId,
|
|
705
778
|
userId: this.userId,
|
|
@@ -712,7 +785,8 @@ var ApprovalService = class {
|
|
|
712
785
|
// Revoke Methods
|
|
713
786
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
714
787
|
/**
|
|
715
|
-
* Revoke
|
|
788
|
+
* Revoke the caller's approvals of a tool: its session, user, time-limited and context
|
|
789
|
+
* approvals. Returns whether anything was revoked. Recorded denials are kept.
|
|
716
790
|
*/
|
|
717
791
|
async revokeApproval(toolId, options = {}) {
|
|
718
792
|
return this.store.revokeApproval({
|
|
@@ -731,18 +805,18 @@ var ApprovalService = class {
|
|
|
731
805
|
}
|
|
732
806
|
};
|
|
733
807
|
ApprovalService = __decorateClass([
|
|
734
|
-
(0,
|
|
808
|
+
(0, import_sdk2.Provider)({
|
|
735
809
|
name: "provider:approval:service",
|
|
736
810
|
description: "Service for managing tool approvals",
|
|
737
|
-
scope:
|
|
811
|
+
scope: import_sdk2.ProviderScope.CONTEXT
|
|
738
812
|
})
|
|
739
813
|
], ApprovalService);
|
|
740
|
-
function createApprovalService(store, sessionId, userId) {
|
|
741
|
-
return new ApprovalService(store, sessionId, userId);
|
|
814
|
+
function createApprovalService(store, sessionId, userId, requirementOf) {
|
|
815
|
+
return new ApprovalService(store, sessionId, userId, requirementOf);
|
|
742
816
|
}
|
|
743
817
|
|
|
744
818
|
// plugins/plugin-approval/src/services/challenge.service.ts
|
|
745
|
-
var
|
|
819
|
+
var import_sdk3 = require("@frontmcp/sdk");
|
|
746
820
|
var import_utils2 = require("@frontmcp/utils");
|
|
747
821
|
var ChallengeService = class {
|
|
748
822
|
storage;
|
|
@@ -876,10 +950,10 @@ var ChallengeService = class {
|
|
|
876
950
|
}
|
|
877
951
|
};
|
|
878
952
|
ChallengeService = __decorateClass([
|
|
879
|
-
(0,
|
|
953
|
+
(0, import_sdk3.Provider)({
|
|
880
954
|
name: "provider:approval:challenge-service",
|
|
881
955
|
description: "PKCE challenge service for webhook approval flows",
|
|
882
|
-
scope:
|
|
956
|
+
scope: import_sdk3.ProviderScope.GLOBAL
|
|
883
957
|
})
|
|
884
958
|
], ChallengeService);
|
|
885
959
|
function generatePkcePairFromVerifier(codeVerifier) {
|
|
@@ -897,11 +971,12 @@ function createMemoryChallengeService(options = {}) {
|
|
|
897
971
|
}
|
|
898
972
|
|
|
899
973
|
// plugins/plugin-approval/src/stores/approval-storage.store.ts
|
|
900
|
-
var
|
|
974
|
+
var import_sdk4 = require("@frontmcp/sdk");
|
|
901
975
|
var import_utils3 = require("@frontmcp/utils");
|
|
902
976
|
function escapePattern(str) {
|
|
903
977
|
return str.replace(/[*?[\]\\]/g, "\\$&");
|
|
904
978
|
}
|
|
979
|
+
var TIME_LIMITED_KEY_SEGMENT = `scope:${"time_limited" /* TIME_LIMITED */}`;
|
|
905
980
|
var ApprovalStorageStore = class {
|
|
906
981
|
storage;
|
|
907
982
|
options;
|
|
@@ -951,6 +1026,22 @@ var ApprovalStorageStore = class {
|
|
|
951
1026
|
if (context) parts.push(`ctx:${context.type}:${context.identifier}`);
|
|
952
1027
|
return parts.join(":");
|
|
953
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* The key of a time-limited approval granted to a session alone or a user alone. Kept apart from
|
|
1031
|
+
* that session's or user's own key, so a time-limited grant never replaces the session or user
|
|
1032
|
+
* approval, or a denial recorded there. (With both a session and a user, `buildKey` is already distinct.)
|
|
1033
|
+
*/
|
|
1034
|
+
timeLimitedKey(toolId, sessionId, userId) {
|
|
1035
|
+
return `${this.buildKey(toolId, sessionId, userId)}:${TIME_LIMITED_KEY_SEGMENT}`;
|
|
1036
|
+
}
|
|
1037
|
+
/** The key a new record is stored under. */
|
|
1038
|
+
recordKey(options) {
|
|
1039
|
+
const { toolId, scope, sessionId, userId, context } = options;
|
|
1040
|
+
if (scope === "time_limited" /* TIME_LIMITED */ && !context && !(sessionId && userId)) {
|
|
1041
|
+
return this.timeLimitedKey(toolId, sessionId, userId);
|
|
1042
|
+
}
|
|
1043
|
+
return this.buildKey(toolId, sessionId, userId, context);
|
|
1044
|
+
}
|
|
954
1045
|
parseRecord(value) {
|
|
955
1046
|
if (!value) return void 0;
|
|
956
1047
|
try {
|
|
@@ -968,28 +1059,55 @@ var ApprovalStorageStore = class {
|
|
|
968
1059
|
return approval.expiresAt !== void 0 && Date.now() > approval.expiresAt;
|
|
969
1060
|
}
|
|
970
1061
|
/**
|
|
971
|
-
* The
|
|
972
|
-
*
|
|
1062
|
+
* The keys a caller's approvals of a tool can be stored under: its session, its user, both
|
|
1063
|
+
* (time-limited and context grants made through `ApprovalService`), the time-limited keys of its
|
|
1064
|
+
* session and its user, and, for a call in a server-established context, that context (granted
|
|
1065
|
+
* to the session and the user, or to the session before it had a user, the way a session approval
|
|
1066
|
+
* stays in force once the caller's user is known). Records written by 1.8.2 sit under the same
|
|
1067
|
+
* keys (a time-limited grant without a user under the session key), so they are still read.
|
|
973
1068
|
*/
|
|
974
|
-
|
|
975
|
-
this.
|
|
976
|
-
const keys = [this.buildKey(toolId, sessionId)];
|
|
1069
|
+
callerKeys(toolId, sessionId, userId, context) {
|
|
1070
|
+
const keys = [this.buildKey(toolId, sessionId), this.timeLimitedKey(toolId, sessionId)];
|
|
977
1071
|
if (userId) {
|
|
978
|
-
keys.push(
|
|
1072
|
+
keys.push(
|
|
1073
|
+
this.buildKey(toolId, void 0, userId),
|
|
1074
|
+
this.timeLimitedKey(toolId, void 0, userId),
|
|
1075
|
+
this.buildKey(toolId, sessionId, userId)
|
|
1076
|
+
);
|
|
979
1077
|
}
|
|
980
|
-
|
|
981
|
-
|
|
1078
|
+
if (context) {
|
|
1079
|
+
keys.push(this.buildKey(toolId, sessionId, void 0, context));
|
|
1080
|
+
if (userId) keys.push(this.buildKey(toolId, sessionId, userId, context));
|
|
1081
|
+
}
|
|
1082
|
+
return [...new Set(keys)];
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Every unexpired record that applies to this caller: session, user, time-limited and, when
|
|
1086
|
+
* `context` is given, context-specific approvals and denials.
|
|
1087
|
+
*/
|
|
1088
|
+
async getApprovals(toolId, sessionId, userId, context) {
|
|
1089
|
+
this.ensureInitialized();
|
|
1090
|
+
const records = [];
|
|
1091
|
+
for (const key of this.callerKeys(toolId, sessionId, userId, context)) {
|
|
982
1092
|
const record = this.parseRecord(await this.storage.get(key));
|
|
983
|
-
if (record && !this.isExpired(record)) {
|
|
984
|
-
|
|
1093
|
+
if (record && record.toolId === toolId && !this.isExpired(record)) {
|
|
1094
|
+
records.push(record);
|
|
985
1095
|
}
|
|
986
1096
|
}
|
|
987
|
-
return
|
|
1097
|
+
return records;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* The caller's record for a tool; a denial in any of its scopes wins, so an approval in one
|
|
1101
|
+
* scope cannot mask a denial in another.
|
|
1102
|
+
*/
|
|
1103
|
+
async getApproval(toolId, sessionId, userId, context) {
|
|
1104
|
+
const records = await this.getApprovals(toolId, sessionId, userId, context);
|
|
1105
|
+
return records.find((record) => record.state === "denied" /* DENIED */) ?? records[0];
|
|
988
1106
|
}
|
|
989
1107
|
async queryApprovals(query) {
|
|
990
1108
|
this.ensureInitialized();
|
|
991
1109
|
const results = [];
|
|
992
|
-
const pattern = query.toolId ? `${query.toolId}:*` : "*";
|
|
1110
|
+
const pattern = query.toolId ? `${escapePattern(query.toolId)}:*` : "*";
|
|
993
1111
|
const keys = await this.storage.keys(pattern);
|
|
994
1112
|
const values = await this.storage.mget(keys);
|
|
995
1113
|
for (const value of values) {
|
|
@@ -1017,8 +1135,12 @@ var ApprovalStorageStore = class {
|
|
|
1017
1135
|
}
|
|
1018
1136
|
async grantApproval(options) {
|
|
1019
1137
|
this.ensureInitialized();
|
|
1138
|
+
assertValidTtl(options.ttlMs);
|
|
1139
|
+
if (options.scope === "time_limited" /* TIME_LIMITED */ && options.ttlMs === void 0) {
|
|
1140
|
+
throw new ApprovalOperationError("grant", "a time-limited approval needs ttlMs");
|
|
1141
|
+
}
|
|
1020
1142
|
const now = Date.now();
|
|
1021
|
-
const expiresAt = options.ttlMs ? now + options.ttlMs : void 0;
|
|
1143
|
+
const expiresAt = options.ttlMs !== void 0 ? now + options.ttlMs : void 0;
|
|
1022
1144
|
const grantedBy = normalizeGrantor(options.grantedBy);
|
|
1023
1145
|
const record = {
|
|
1024
1146
|
toolId: options.toolId,
|
|
@@ -1034,52 +1156,59 @@ var ApprovalStorageStore = class {
|
|
|
1034
1156
|
reason: options.reason,
|
|
1035
1157
|
metadata: options.metadata
|
|
1036
1158
|
};
|
|
1037
|
-
const key = this.
|
|
1038
|
-
const ttlSeconds = options.ttlMs ? Math.ceil(options.ttlMs / 1e3) : void 0;
|
|
1159
|
+
const key = this.recordKey(options);
|
|
1160
|
+
const ttlSeconds = options.ttlMs !== void 0 ? Math.ceil(options.ttlMs / 1e3) : void 0;
|
|
1039
1161
|
await this.storage.set(key, JSON.stringify(record), { ttlSeconds });
|
|
1040
1162
|
return record;
|
|
1041
1163
|
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Deletes the approvals of a tool that belong to the given session or user: with a context, only
|
|
1166
|
+
* that context's approval; otherwise every approval of the tool stored for that session or user
|
|
1167
|
+
* (session, user, time-limited and context approvals alike). Recorded denials are kept.
|
|
1168
|
+
*/
|
|
1042
1169
|
async revokeApproval(options) {
|
|
1043
1170
|
this.ensureInitialized();
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
await this.storage.delete(key);
|
|
1048
|
-
return true;
|
|
1171
|
+
const { toolId, sessionId, userId, context } = options;
|
|
1172
|
+
if (!sessionId && !userId && !context) {
|
|
1173
|
+
return false;
|
|
1049
1174
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1175
|
+
const keys = await this.storage.keys(`${escapePattern(toolId)}:*`);
|
|
1176
|
+
const values = await this.storage.mget(keys);
|
|
1177
|
+
const keysToDelete = [];
|
|
1178
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1179
|
+
const record = this.parseRecord(values[i]);
|
|
1180
|
+
if (!record || record.toolId !== toolId || record.state === "denied" /* DENIED */) continue;
|
|
1181
|
+
if (context) {
|
|
1182
|
+
const sameSession = sessionId !== void 0 && record.sessionId === sessionId;
|
|
1183
|
+
if (record.context?.type === context.type && record.context.identifier === context.identifier && (!sessionId || sameSession) && // A context approval granted to the session before it had a user applies to that user too.
|
|
1184
|
+
(!userId || record.userId === userId || sameSession && record.userId === void 0)) {
|
|
1185
|
+
keysToDelete.push(keys[i]);
|
|
1186
|
+
}
|
|
1187
|
+
continue;
|
|
1060
1188
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const sessionValue = await this.storage.get(sessionKey);
|
|
1064
|
-
const sessionApproval = this.parseRecord(sessionValue);
|
|
1065
|
-
if (sessionApproval && sessionApproval.state === "approved" /* APPROVED */ && !this.isExpired(sessionApproval)) {
|
|
1066
|
-
return true;
|
|
1067
|
-
}
|
|
1068
|
-
if (userId) {
|
|
1069
|
-
const userKey = this.buildKey(toolId, void 0, userId);
|
|
1070
|
-
const userValue = await this.storage.get(userKey);
|
|
1071
|
-
const userApproval = this.parseRecord(userValue);
|
|
1072
|
-
if (userApproval && userApproval.state === "approved" /* APPROVED */ && !this.isExpired(userApproval)) {
|
|
1073
|
-
return true;
|
|
1189
|
+
if (sessionId && record.sessionId === sessionId || userId && record.userId === userId) {
|
|
1190
|
+
keysToDelete.push(keys[i]);
|
|
1074
1191
|
}
|
|
1075
1192
|
}
|
|
1076
|
-
|
|
1193
|
+
if (keysToDelete.length === 0) {
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
await this.storage.mdelete(keysToDelete);
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
async isApproved(toolId, sessionId, userId, context) {
|
|
1200
|
+
const approval = await this.getApproval(toolId, sessionId, userId, context);
|
|
1201
|
+
return approval?.state === "approved" /* APPROVED */;
|
|
1077
1202
|
}
|
|
1078
1203
|
async clearSessionApprovals(sessionId) {
|
|
1079
1204
|
this.ensureInitialized();
|
|
1080
1205
|
const escapedSessionId = escapePattern(sessionId);
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1206
|
+
const keys = [
|
|
1207
|
+
.../* @__PURE__ */ new Set([
|
|
1208
|
+
...await this.storage.keys(`*:session:${escapedSessionId}`),
|
|
1209
|
+
...await this.storage.keys(`*:session:${escapedSessionId}:*`)
|
|
1210
|
+
])
|
|
1211
|
+
];
|
|
1083
1212
|
if (keys.length === 0) {
|
|
1084
1213
|
return 0;
|
|
1085
1214
|
}
|
|
@@ -1146,10 +1275,10 @@ var ApprovalStorageStore = class {
|
|
|
1146
1275
|
}
|
|
1147
1276
|
};
|
|
1148
1277
|
ApprovalStorageStore = __decorateClass([
|
|
1149
|
-
(0,
|
|
1278
|
+
(0, import_sdk4.Provider)({
|
|
1150
1279
|
name: "provider:approval:store:storage",
|
|
1151
1280
|
description: "Storage-backed approval store (supports Memory, Redis, Vercel KV, Upstash)",
|
|
1152
|
-
scope:
|
|
1281
|
+
scope: import_sdk4.ProviderScope.GLOBAL
|
|
1153
1282
|
})
|
|
1154
1283
|
], ApprovalStorageStore);
|
|
1155
1284
|
function createApprovalMemoryStore(options = {}) {
|
|
@@ -1161,7 +1290,7 @@ function createApprovalMemoryStore(options = {}) {
|
|
|
1161
1290
|
}
|
|
1162
1291
|
|
|
1163
1292
|
// plugins/plugin-approval/src/approval.plugin.ts
|
|
1164
|
-
var ApprovalPlugin = class extends
|
|
1293
|
+
var ApprovalPlugin = class extends import_sdk5.DynamicPlugin {
|
|
1165
1294
|
options;
|
|
1166
1295
|
constructor(options = {}) {
|
|
1167
1296
|
super();
|
|
@@ -1222,17 +1351,23 @@ __publicField(ApprovalPlugin, "dynamicProviders", (options) => {
|
|
|
1222
1351
|
providers.push({
|
|
1223
1352
|
name: "approval:service",
|
|
1224
1353
|
provide: ApprovalServiceToken,
|
|
1225
|
-
scope:
|
|
1226
|
-
inject: () => [ApprovalStoreToken,
|
|
1227
|
-
useFactory: (store, ctx) => {
|
|
1354
|
+
scope: import_sdk5.ProviderScope.CONTEXT,
|
|
1355
|
+
inject: () => [ApprovalStoreToken, import_sdk5.FRONTMCP_CONTEXT, import_sdk5.ScopeEntry],
|
|
1356
|
+
useFactory: (store, ctx, scope) => {
|
|
1228
1357
|
const { sessionId, userId } = resolveApprovalIdentity(ctx);
|
|
1229
|
-
|
|
1358
|
+
const requirementOf = (toolId) => {
|
|
1359
|
+
const tool = scope.tools.getTools(true).find((entry) => entry.fullName === toolId);
|
|
1360
|
+
if (!tool) return void 0;
|
|
1361
|
+
const approval = tool.metadata.approval;
|
|
1362
|
+
return resolveApprovalRequirement(approval);
|
|
1363
|
+
};
|
|
1364
|
+
return createApprovalService(store, sessionId, userId, requirementOf);
|
|
1230
1365
|
}
|
|
1231
1366
|
});
|
|
1232
1367
|
return providers;
|
|
1233
1368
|
});
|
|
1234
1369
|
ApprovalPlugin = __decorateClass([
|
|
1235
|
-
(0,
|
|
1370
|
+
(0, import_sdk5.Plugin)({
|
|
1236
1371
|
name: "approval",
|
|
1237
1372
|
description: "Tool authorization workflow with PKCE webhook security",
|
|
1238
1373
|
plugins: [ApprovalCheckPlugin],
|