@frontmcp/plugin-approval 1.8.2 → 1.8.4
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.plugin.d.ts.map +1 -1
- package/esm/index.mjs +373 -236
- 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 +371 -235
- 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
|
@@ -78,6 +78,205 @@ module.exports = __toCommonJS(index_exports);
|
|
|
78
78
|
// plugins/plugin-approval/src/approval.plugin.ts
|
|
79
79
|
var import_sdk5 = require("@frontmcp/sdk");
|
|
80
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
|
+
}
|
|
279
|
+
|
|
81
280
|
// plugins/plugin-approval/src/approval.identity.ts
|
|
82
281
|
var import_utils = require("@frontmcp/utils");
|
|
83
282
|
function nonEmptyString(value) {
|
|
@@ -107,23 +306,6 @@ var ChallengeServiceToken = /* @__PURE__ */ Symbol.for(
|
|
|
107
306
|
// plugins/plugin-approval/src/hooks/approval-check.hook.ts
|
|
108
307
|
var import_sdk = require("@frontmcp/sdk");
|
|
109
308
|
|
|
110
|
-
// plugins/plugin-approval/src/approval/types.ts
|
|
111
|
-
var ApprovalScope = /* @__PURE__ */ ((ApprovalScope2) => {
|
|
112
|
-
ApprovalScope2["SESSION"] = "session";
|
|
113
|
-
ApprovalScope2["USER"] = "user";
|
|
114
|
-
ApprovalScope2["TIME_LIMITED"] = "time_limited";
|
|
115
|
-
ApprovalScope2["TOOL_SPECIFIC"] = "tool_specific";
|
|
116
|
-
ApprovalScope2["CONTEXT_SPECIFIC"] = "context_specific";
|
|
117
|
-
return ApprovalScope2;
|
|
118
|
-
})(ApprovalScope || {});
|
|
119
|
-
var ApprovalState = /* @__PURE__ */ ((ApprovalState2) => {
|
|
120
|
-
ApprovalState2["PENDING"] = "pending";
|
|
121
|
-
ApprovalState2["APPROVED"] = "approved";
|
|
122
|
-
ApprovalState2["DENIED"] = "denied";
|
|
123
|
-
ApprovalState2["EXPIRED"] = "expired";
|
|
124
|
-
return ApprovalState2;
|
|
125
|
-
})(ApprovalState || {});
|
|
126
|
-
|
|
127
309
|
// plugins/plugin-approval/src/approval/schemas.ts
|
|
128
310
|
var import_lazy_zod = require("@frontmcp/lazy-zod");
|
|
129
311
|
var approvalScopeSchema = import_lazy_zod.z.nativeEnum(ApprovalScope);
|
|
@@ -340,136 +522,6 @@ function isApiGrantor(grantor) {
|
|
|
340
522
|
return grantor.source === "api" || grantor.source === "oauth";
|
|
341
523
|
}
|
|
342
524
|
|
|
343
|
-
// plugins/plugin-approval/src/approval/errors.ts
|
|
344
|
-
var ApprovalError = class extends Error {
|
|
345
|
-
constructor(message) {
|
|
346
|
-
super(message);
|
|
347
|
-
this.name = "ApprovalError";
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
var ApprovalRequiredError = class extends ApprovalError {
|
|
351
|
-
constructor(details) {
|
|
352
|
-
super(details.message);
|
|
353
|
-
this.details = details;
|
|
354
|
-
this.name = "ApprovalRequiredError";
|
|
355
|
-
}
|
|
356
|
-
details;
|
|
357
|
-
/**
|
|
358
|
-
* Convert to a JSON-RPC compatible error structure.
|
|
359
|
-
*/
|
|
360
|
-
toJsonRpcError() {
|
|
361
|
-
return {
|
|
362
|
-
code: -32600,
|
|
363
|
-
// Invalid Request
|
|
364
|
-
message: this.details.message,
|
|
365
|
-
data: {
|
|
366
|
-
type: "approval_required",
|
|
367
|
-
toolId: this.details.toolId,
|
|
368
|
-
state: this.details.state,
|
|
369
|
-
options: this.details.approvalOptions
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
var ApprovalOperationError = class extends ApprovalError {
|
|
375
|
-
constructor(operation, reason) {
|
|
376
|
-
super(`Approval ${operation} failed: ${reason}`);
|
|
377
|
-
this.operation = operation;
|
|
378
|
-
this.reason = reason;
|
|
379
|
-
this.name = "ApprovalOperationError";
|
|
380
|
-
}
|
|
381
|
-
operation;
|
|
382
|
-
reason;
|
|
383
|
-
/**
|
|
384
|
-
* Convert to a JSON-RPC compatible error structure.
|
|
385
|
-
*/
|
|
386
|
-
toJsonRpcError() {
|
|
387
|
-
return {
|
|
388
|
-
code: -32603,
|
|
389
|
-
// Internal Error
|
|
390
|
-
message: "Approval operation failed",
|
|
391
|
-
data: {
|
|
392
|
-
type: "approval_operation_error",
|
|
393
|
-
operation: this.operation
|
|
394
|
-
}
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
var ApprovalScopeNotAllowedError = class extends ApprovalError {
|
|
399
|
-
constructor(requestedScope, allowedScopes) {
|
|
400
|
-
super(
|
|
401
|
-
`Approval scope '${requestedScope}' is not allowed for this tool. Allowed scopes: ${allowedScopes.join(", ")}`
|
|
402
|
-
);
|
|
403
|
-
this.requestedScope = requestedScope;
|
|
404
|
-
this.allowedScopes = allowedScopes;
|
|
405
|
-
this.name = "ApprovalScopeNotAllowedError";
|
|
406
|
-
}
|
|
407
|
-
requestedScope;
|
|
408
|
-
allowedScopes;
|
|
409
|
-
/**
|
|
410
|
-
* Convert to a JSON-RPC compatible error structure.
|
|
411
|
-
*/
|
|
412
|
-
toJsonRpcError() {
|
|
413
|
-
return {
|
|
414
|
-
code: -32602,
|
|
415
|
-
// Invalid Params
|
|
416
|
-
message: this.message,
|
|
417
|
-
data: {
|
|
418
|
-
type: "approval_scope_not_allowed",
|
|
419
|
-
requestedScope: this.requestedScope,
|
|
420
|
-
allowedScopes: this.allowedScopes
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
var ApprovalExpiredError = class extends ApprovalError {
|
|
426
|
-
constructor(toolId, expiredAt) {
|
|
427
|
-
super(`Approval for tool '${toolId}' expired at ${new Date(expiredAt).toISOString()}`);
|
|
428
|
-
this.toolId = toolId;
|
|
429
|
-
this.expiredAt = expiredAt;
|
|
430
|
-
this.name = "ApprovalExpiredError";
|
|
431
|
-
}
|
|
432
|
-
toolId;
|
|
433
|
-
expiredAt;
|
|
434
|
-
/**
|
|
435
|
-
* Convert to a JSON-RPC compatible error structure.
|
|
436
|
-
*/
|
|
437
|
-
toJsonRpcError() {
|
|
438
|
-
return {
|
|
439
|
-
code: -32600,
|
|
440
|
-
// Invalid Request
|
|
441
|
-
message: this.message,
|
|
442
|
-
data: {
|
|
443
|
-
type: "approval_expired",
|
|
444
|
-
toolId: this.toolId,
|
|
445
|
-
expiredAt: this.expiredAt
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
var ChallengeValidationError = class extends ApprovalError {
|
|
451
|
-
constructor(reason = "invalid", message) {
|
|
452
|
-
super(message ?? `PKCE challenge validation failed: ${reason}`);
|
|
453
|
-
this.reason = reason;
|
|
454
|
-
this.name = "ChallengeValidationError";
|
|
455
|
-
}
|
|
456
|
-
reason;
|
|
457
|
-
/**
|
|
458
|
-
* Convert to a JSON-RPC compatible error structure.
|
|
459
|
-
*/
|
|
460
|
-
toJsonRpcError() {
|
|
461
|
-
return {
|
|
462
|
-
code: -32600,
|
|
463
|
-
// Invalid Request
|
|
464
|
-
message: this.message,
|
|
465
|
-
data: {
|
|
466
|
-
type: "challenge_validation_error",
|
|
467
|
-
reason: this.reason
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
|
|
473
525
|
// plugins/plugin-approval/src/hooks/approval-check.hook.ts
|
|
474
526
|
var passedApprovalStores = /* @__PURE__ */ new WeakMap();
|
|
475
527
|
var ApprovalCheckPlugin = class extends import_sdk.DynamicPlugin {
|
|
@@ -477,7 +529,7 @@ var ApprovalCheckPlugin = class extends import_sdk.DynamicPlugin {
|
|
|
477
529
|
const { tool, toolContext } = flowCtx.state;
|
|
478
530
|
if (!tool || !toolContext) return;
|
|
479
531
|
const metadata = tool.metadata;
|
|
480
|
-
const approvalConfig =
|
|
532
|
+
const approvalConfig = resolveApprovalRequirement(
|
|
481
533
|
metadata["approval"]
|
|
482
534
|
);
|
|
483
535
|
if (!approvalConfig.required) {
|
|
@@ -495,44 +547,43 @@ var ApprovalCheckPlugin = class extends import_sdk.DynamicPlugin {
|
|
|
495
547
|
}
|
|
496
548
|
async enforceApproval(flowCtx, tool, toolContext, approvalConfig, approvalStore) {
|
|
497
549
|
const { sessionId, userId } = resolveApprovalIdentity(toolContext.tryGetContext?.());
|
|
498
|
-
const
|
|
499
|
-
|
|
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 */)) {
|
|
500
553
|
throw new ApprovalRequiredError({
|
|
501
554
|
toolId: tool.fullName,
|
|
502
555
|
state: "denied",
|
|
503
556
|
message: `Tool "${tool.fullName}" execution denied.`
|
|
504
557
|
});
|
|
505
558
|
}
|
|
506
|
-
if (this.isPreApprovedContext(approvalConfig,
|
|
559
|
+
if (this.isPreApprovedContext(approvalConfig, currentContext)) {
|
|
507
560
|
return;
|
|
508
561
|
}
|
|
562
|
+
const approved = records.filter((record) => record.state === "approved" /* APPROVED */);
|
|
509
563
|
if (approvalConfig.alwaysPrompt) {
|
|
510
|
-
await this.handleApprovalRequired(flowCtx, approvalConfig,
|
|
564
|
+
await this.handleApprovalRequired(flowCtx, approvalConfig, approved[0]);
|
|
511
565
|
return;
|
|
512
566
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
567
|
+
const now = Date.now();
|
|
568
|
+
if (approved.some((record) => isApprovalUsable(record, approvalConfig, now))) {
|
|
569
|
+
return;
|
|
517
570
|
}
|
|
518
|
-
await this.handleApprovalRequired(
|
|
571
|
+
await this.handleApprovalRequired(
|
|
572
|
+
flowCtx,
|
|
573
|
+
approvalConfig,
|
|
574
|
+
approved.find((record) => isApprovalExpired(record, approvalConfig, now)) ?? approved[0]
|
|
575
|
+
);
|
|
519
576
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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);
|
|
526
584
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
required: config.required ?? true,
|
|
530
|
-
defaultScope: config.defaultScope ?? "session" /* SESSION */
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
isExpired(approval) {
|
|
534
|
-
if (!approval.expiresAt) return false;
|
|
535
|
-
return Date.now() > approval.expiresAt;
|
|
585
|
+
const record = await store.getApproval(toolId, sessionId, userId, context);
|
|
586
|
+
return record ? [record] : [];
|
|
536
587
|
}
|
|
537
588
|
/**
|
|
538
589
|
* The context this call is running in, as established by the SESSION (GHSA-r848-p7wf-96rc).
|
|
@@ -562,7 +613,7 @@ var ApprovalCheckPlugin = class extends import_sdk.DynamicPlugin {
|
|
|
562
613
|
async handleApprovalRequired(flowCtx, config, existingApproval) {
|
|
563
614
|
const { tool } = flowCtx.state;
|
|
564
615
|
const message = config.approvalMessage ?? `Tool "${tool?.fullName}" requires approval to execute. Allow?`;
|
|
565
|
-
const isExpiredApproval = existingApproval ?
|
|
616
|
+
const isExpiredApproval = existingApproval ? isApprovalExpired(existingApproval, config, Date.now()) : false;
|
|
566
617
|
throw new ApprovalRequiredError({
|
|
567
618
|
toolId: tool?.fullName ?? "unknown",
|
|
568
619
|
state: isExpiredApproval ? "expired" : "pending",
|
|
@@ -578,26 +629,34 @@ var ApprovalCheckPlugin = class extends import_sdk.DynamicPlugin {
|
|
|
578
629
|
}
|
|
579
630
|
};
|
|
580
631
|
__decorateClass([
|
|
581
|
-
import_sdk.ToolHook.Will("execute", { priority: 100 })
|
|
632
|
+
import_sdk.ToolHook.Will("execute", { priority: 100, appliesTo: "uncovered-apps" })
|
|
582
633
|
], ApprovalCheckPlugin.prototype, "checkApproval", 1);
|
|
583
634
|
ApprovalCheckPlugin = __decorateClass([
|
|
584
635
|
(0, import_sdk.Plugin)({
|
|
585
636
|
name: "approval:check",
|
|
586
|
-
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"]
|
|
587
640
|
})
|
|
588
641
|
], ApprovalCheckPlugin);
|
|
589
642
|
|
|
590
643
|
// plugins/plugin-approval/src/services/approval.service.ts
|
|
591
644
|
var import_sdk2 = require("@frontmcp/sdk");
|
|
592
645
|
var ApprovalService = class {
|
|
593
|
-
constructor(store, sessionId, userId) {
|
|
646
|
+
constructor(store, sessionId, userId, requirementOf = () => void 0) {
|
|
594
647
|
this.store = store;
|
|
595
648
|
this.sessionId = sessionId;
|
|
596
649
|
this.userId = userId;
|
|
650
|
+
this.requirementOf = requirementOf;
|
|
597
651
|
}
|
|
598
652
|
store;
|
|
599
653
|
sessionId;
|
|
600
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
|
+
}
|
|
601
660
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
602
661
|
// Query Methods
|
|
603
662
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -605,7 +664,14 @@ var ApprovalService = class {
|
|
|
605
664
|
* Check if a tool is approved for current session/user.
|
|
606
665
|
*/
|
|
607
666
|
async isApproved(toolId, context) {
|
|
608
|
-
|
|
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));
|
|
609
675
|
}
|
|
610
676
|
/**
|
|
611
677
|
* Get approval record for a tool.
|
|
@@ -636,14 +702,15 @@ var ApprovalService = class {
|
|
|
636
702
|
});
|
|
637
703
|
}
|
|
638
704
|
/**
|
|
639
|
-
* 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.
|
|
640
708
|
*/
|
|
641
709
|
async queryApprovals(query) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
sessionId
|
|
645
|
-
|
|
646
|
-
});
|
|
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
|
+
);
|
|
647
714
|
}
|
|
648
715
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
649
716
|
// Grant Methods
|
|
@@ -652,9 +719,11 @@ var ApprovalService = class {
|
|
|
652
719
|
* Grant session-scoped approval for a tool.
|
|
653
720
|
*/
|
|
654
721
|
async grantSessionApproval(toolId, options = {}) {
|
|
722
|
+
const ttlMs = this.checkGrant(toolId, "session" /* SESSION */);
|
|
655
723
|
return this.store.grantApproval({
|
|
656
724
|
toolId,
|
|
657
725
|
scope: "session" /* SESSION */,
|
|
726
|
+
ttlMs,
|
|
658
727
|
sessionId: this.sessionId,
|
|
659
728
|
grantedBy: options.grantedBy ?? "policy",
|
|
660
729
|
reason: options.reason,
|
|
@@ -668,9 +737,11 @@ var ApprovalService = class {
|
|
|
668
737
|
if (!this.userId) {
|
|
669
738
|
throw new Error("Cannot grant user approval without userId");
|
|
670
739
|
}
|
|
740
|
+
const ttlMs = this.checkGrant(toolId, "user" /* USER */);
|
|
671
741
|
return this.store.grantApproval({
|
|
672
742
|
toolId,
|
|
673
743
|
scope: "user" /* USER */,
|
|
744
|
+
ttlMs,
|
|
674
745
|
userId: this.userId,
|
|
675
746
|
grantedBy: options.grantedBy ?? "policy",
|
|
676
747
|
reason: options.reason,
|
|
@@ -681,6 +752,7 @@ var ApprovalService = class {
|
|
|
681
752
|
* Grant time-limited approval for a tool.
|
|
682
753
|
*/
|
|
683
754
|
async grantTimeLimitedApproval(toolId, ttlMs, options = {}) {
|
|
755
|
+
this.checkGrant(toolId, "time_limited" /* TIME_LIMITED */, ttlMs);
|
|
684
756
|
return this.store.grantApproval({
|
|
685
757
|
toolId,
|
|
686
758
|
scope: "time_limited" /* TIME_LIMITED */,
|
|
@@ -696,9 +768,11 @@ var ApprovalService = class {
|
|
|
696
768
|
* Grant context-specific approval for a tool.
|
|
697
769
|
*/
|
|
698
770
|
async grantContextApproval(toolId, context, options = {}) {
|
|
771
|
+
const ttlMs = this.checkGrant(toolId, "context_specific" /* CONTEXT_SPECIFIC */);
|
|
699
772
|
return this.store.grantApproval({
|
|
700
773
|
toolId,
|
|
701
774
|
scope: "context_specific" /* CONTEXT_SPECIFIC */,
|
|
775
|
+
ttlMs,
|
|
702
776
|
context,
|
|
703
777
|
sessionId: this.sessionId,
|
|
704
778
|
userId: this.userId,
|
|
@@ -711,7 +785,8 @@ var ApprovalService = class {
|
|
|
711
785
|
// Revoke Methods
|
|
712
786
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
713
787
|
/**
|
|
714
|
-
* 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.
|
|
715
790
|
*/
|
|
716
791
|
async revokeApproval(toolId, options = {}) {
|
|
717
792
|
return this.store.revokeApproval({
|
|
@@ -736,8 +811,8 @@ ApprovalService = __decorateClass([
|
|
|
736
811
|
scope: import_sdk2.ProviderScope.CONTEXT
|
|
737
812
|
})
|
|
738
813
|
], ApprovalService);
|
|
739
|
-
function createApprovalService(store, sessionId, userId) {
|
|
740
|
-
return new ApprovalService(store, sessionId, userId);
|
|
814
|
+
function createApprovalService(store, sessionId, userId, requirementOf) {
|
|
815
|
+
return new ApprovalService(store, sessionId, userId, requirementOf);
|
|
741
816
|
}
|
|
742
817
|
|
|
743
818
|
// plugins/plugin-approval/src/services/challenge.service.ts
|
|
@@ -901,6 +976,7 @@ var import_utils3 = require("@frontmcp/utils");
|
|
|
901
976
|
function escapePattern(str) {
|
|
902
977
|
return str.replace(/[*?[\]\\]/g, "\\$&");
|
|
903
978
|
}
|
|
979
|
+
var TIME_LIMITED_KEY_SEGMENT = `scope:${"time_limited" /* TIME_LIMITED */}`;
|
|
904
980
|
var ApprovalStorageStore = class {
|
|
905
981
|
storage;
|
|
906
982
|
options;
|
|
@@ -950,6 +1026,22 @@ var ApprovalStorageStore = class {
|
|
|
950
1026
|
if (context) parts.push(`ctx:${context.type}:${context.identifier}`);
|
|
951
1027
|
return parts.join(":");
|
|
952
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
|
+
}
|
|
953
1045
|
parseRecord(value) {
|
|
954
1046
|
if (!value) return void 0;
|
|
955
1047
|
try {
|
|
@@ -967,28 +1059,55 @@ var ApprovalStorageStore = class {
|
|
|
967
1059
|
return approval.expiresAt !== void 0 && Date.now() > approval.expiresAt;
|
|
968
1060
|
}
|
|
969
1061
|
/**
|
|
970
|
-
* The
|
|
971
|
-
*
|
|
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.
|
|
972
1068
|
*/
|
|
973
|
-
|
|
974
|
-
this.
|
|
975
|
-
const keys = [this.buildKey(toolId, sessionId)];
|
|
1069
|
+
callerKeys(toolId, sessionId, userId, context) {
|
|
1070
|
+
const keys = [this.buildKey(toolId, sessionId), this.timeLimitedKey(toolId, sessionId)];
|
|
976
1071
|
if (userId) {
|
|
977
|
-
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
|
+
);
|
|
978
1077
|
}
|
|
979
|
-
|
|
980
|
-
|
|
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)) {
|
|
981
1092
|
const record = this.parseRecord(await this.storage.get(key));
|
|
982
|
-
if (record && !this.isExpired(record)) {
|
|
983
|
-
|
|
1093
|
+
if (record && record.toolId === toolId && !this.isExpired(record)) {
|
|
1094
|
+
records.push(record);
|
|
984
1095
|
}
|
|
985
1096
|
}
|
|
986
|
-
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];
|
|
987
1106
|
}
|
|
988
1107
|
async queryApprovals(query) {
|
|
989
1108
|
this.ensureInitialized();
|
|
990
1109
|
const results = [];
|
|
991
|
-
const pattern = query.toolId ? `${query.toolId}:*` : "*";
|
|
1110
|
+
const pattern = query.toolId ? `${escapePattern(query.toolId)}:*` : "*";
|
|
992
1111
|
const keys = await this.storage.keys(pattern);
|
|
993
1112
|
const values = await this.storage.mget(keys);
|
|
994
1113
|
for (const value of values) {
|
|
@@ -1016,8 +1135,12 @@ var ApprovalStorageStore = class {
|
|
|
1016
1135
|
}
|
|
1017
1136
|
async grantApproval(options) {
|
|
1018
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
|
+
}
|
|
1019
1142
|
const now = Date.now();
|
|
1020
|
-
const expiresAt = options.ttlMs ? now + options.ttlMs : void 0;
|
|
1143
|
+
const expiresAt = options.ttlMs !== void 0 ? now + options.ttlMs : void 0;
|
|
1021
1144
|
const grantedBy = normalizeGrantor(options.grantedBy);
|
|
1022
1145
|
const record = {
|
|
1023
1146
|
toolId: options.toolId,
|
|
@@ -1033,52 +1156,59 @@ var ApprovalStorageStore = class {
|
|
|
1033
1156
|
reason: options.reason,
|
|
1034
1157
|
metadata: options.metadata
|
|
1035
1158
|
};
|
|
1036
|
-
const key = this.
|
|
1037
|
-
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;
|
|
1038
1161
|
await this.storage.set(key, JSON.stringify(record), { ttlSeconds });
|
|
1039
1162
|
return record;
|
|
1040
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
|
+
*/
|
|
1041
1169
|
async revokeApproval(options) {
|
|
1042
1170
|
this.ensureInitialized();
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
await this.storage.delete(key);
|
|
1047
|
-
return true;
|
|
1171
|
+
const { toolId, sessionId, userId, context } = options;
|
|
1172
|
+
if (!sessionId && !userId && !context) {
|
|
1173
|
+
return false;
|
|
1048
1174
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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;
|
|
1059
1188
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
const sessionValue = await this.storage.get(sessionKey);
|
|
1063
|
-
const sessionApproval = this.parseRecord(sessionValue);
|
|
1064
|
-
if (sessionApproval && sessionApproval.state === "approved" /* APPROVED */ && !this.isExpired(sessionApproval)) {
|
|
1065
|
-
return true;
|
|
1066
|
-
}
|
|
1067
|
-
if (userId) {
|
|
1068
|
-
const userKey = this.buildKey(toolId, void 0, userId);
|
|
1069
|
-
const userValue = await this.storage.get(userKey);
|
|
1070
|
-
const userApproval = this.parseRecord(userValue);
|
|
1071
|
-
if (userApproval && userApproval.state === "approved" /* APPROVED */ && !this.isExpired(userApproval)) {
|
|
1072
|
-
return true;
|
|
1189
|
+
if (sessionId && record.sessionId === sessionId || userId && record.userId === userId) {
|
|
1190
|
+
keysToDelete.push(keys[i]);
|
|
1073
1191
|
}
|
|
1074
1192
|
}
|
|
1075
|
-
|
|
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 */;
|
|
1076
1202
|
}
|
|
1077
1203
|
async clearSessionApprovals(sessionId) {
|
|
1078
1204
|
this.ensureInitialized();
|
|
1079
1205
|
const escapedSessionId = escapePattern(sessionId);
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1206
|
+
const keys = [
|
|
1207
|
+
.../* @__PURE__ */ new Set([
|
|
1208
|
+
...await this.storage.keys(`*:session:${escapedSessionId}`),
|
|
1209
|
+
...await this.storage.keys(`*:session:${escapedSessionId}:*`)
|
|
1210
|
+
])
|
|
1211
|
+
];
|
|
1082
1212
|
if (keys.length === 0) {
|
|
1083
1213
|
return 0;
|
|
1084
1214
|
}
|
|
@@ -1222,10 +1352,16 @@ __publicField(ApprovalPlugin, "dynamicProviders", (options) => {
|
|
|
1222
1352
|
name: "approval:service",
|
|
1223
1353
|
provide: ApprovalServiceToken,
|
|
1224
1354
|
scope: import_sdk5.ProviderScope.CONTEXT,
|
|
1225
|
-
inject: () => [ApprovalStoreToken, import_sdk5.FRONTMCP_CONTEXT],
|
|
1226
|
-
useFactory: (store, ctx) => {
|
|
1355
|
+
inject: () => [ApprovalStoreToken, import_sdk5.FRONTMCP_CONTEXT, import_sdk5.ScopeEntry],
|
|
1356
|
+
useFactory: (store, ctx, scope) => {
|
|
1227
1357
|
const { sessionId, userId } = resolveApprovalIdentity(ctx);
|
|
1228
|
-
|
|
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);
|
|
1229
1365
|
}
|
|
1230
1366
|
});
|
|
1231
1367
|
return providers;
|