@frontmcp/plugin-approval 1.8.6 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/approval/types.d.ts +2 -1
- package/approval/types.d.ts.map +1 -1
- package/esm/index.mjs +139 -25
- package/esm/package.json +4 -4
- package/hooks/approval-check.hook.d.ts +19 -0
- package/hooks/approval-check.hook.d.ts.map +1 -1
- package/index.js +130 -21
- package/package.json +4 -4
- package/services/approval.service.d.ts +9 -1
- package/services/approval.service.d.ts.map +1 -1
- package/stores/approval-storage.store.d.ts +12 -0
- package/stores/approval-storage.store.d.ts.map +1 -1
- package/stores/approval-store.interface.d.ts +15 -1
- package/stores/approval-store.interface.d.ts.map +1 -1
package/approval/types.d.ts
CHANGED
|
@@ -221,7 +221,8 @@ export interface ToolApprovalRequirement {
|
|
|
221
221
|
maxTtlMs?: number;
|
|
222
222
|
/**
|
|
223
223
|
* Whether to prompt on each call even if approved.
|
|
224
|
-
* For highly sensitive operations
|
|
224
|
+
* For highly sensitive operations: each approval lets exactly one call through and is used up by
|
|
225
|
+
* it, so the next call is refused with `pending` until the caller approves again.
|
|
225
226
|
* @default false
|
|
226
227
|
*/
|
|
227
228
|
alwaysPrompt?: boolean;
|
package/approval/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/approval/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/approval/types.ts"],"names":[],"mappings":"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"}
|
package/esm/index.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
|
|
2
|
+
const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
|
|
3
|
+
// __frontmcpCreateRequire
|
|
1
4
|
var __defProp = Object.defineProperty;
|
|
2
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
6
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
@@ -23,7 +26,7 @@ import {
|
|
|
23
26
|
FRONTMCP_CONTEXT,
|
|
24
27
|
Plugin as Plugin2,
|
|
25
28
|
ProviderScope as ProviderScope4,
|
|
26
|
-
ScopeEntry
|
|
29
|
+
ScopeEntry as ScopeEntry2
|
|
27
30
|
} from "@frontmcp/sdk";
|
|
28
31
|
|
|
29
32
|
// plugins/plugin-approval/src/approval/errors.ts
|
|
@@ -255,7 +258,7 @@ var ChallengeServiceToken = /* @__PURE__ */ Symbol.for(
|
|
|
255
258
|
);
|
|
256
259
|
|
|
257
260
|
// plugins/plugin-approval/src/hooks/approval-check.hook.ts
|
|
258
|
-
import { DynamicPlugin, Plugin, ToolHook } from "@frontmcp/sdk";
|
|
261
|
+
import { DynamicPlugin, isEntryGatedBy, Plugin, ScopeEntry, ToolHook } from "@frontmcp/sdk";
|
|
259
262
|
|
|
260
263
|
// plugins/plugin-approval/src/approval/schemas.ts
|
|
261
264
|
import { z } from "@frontmcp/lazy-zod";
|
|
@@ -474,7 +477,7 @@ function isApiGrantor(grantor) {
|
|
|
474
477
|
}
|
|
475
478
|
|
|
476
479
|
// plugins/plugin-approval/src/hooks/approval-check.hook.ts
|
|
477
|
-
var
|
|
480
|
+
var decidedCalls = /* @__PURE__ */ new WeakSet();
|
|
478
481
|
var ApprovalCheckPlugin = class extends DynamicPlugin {
|
|
479
482
|
async checkApproval(flowCtx) {
|
|
480
483
|
const { tool, toolContext } = flowCtx.state;
|
|
@@ -489,17 +492,19 @@ var ApprovalCheckPlugin = class extends DynamicPlugin {
|
|
|
489
492
|
if (approvalConfig.skipApproval) {
|
|
490
493
|
return;
|
|
491
494
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
passedApprovalStores.set(toolContext, passedStores);
|
|
498
|
-
}
|
|
499
|
-
async enforceApproval(flowCtx, tool, toolContext, approvalConfig, approvalStore) {
|
|
495
|
+
if (decidedCalls.has(toolContext)) return;
|
|
496
|
+
await this.enforceApproval(flowCtx, tool, toolContext, approvalConfig, this.coveringStores(tool));
|
|
497
|
+
decidedCalls.add(toolContext);
|
|
498
|
+
}
|
|
499
|
+
async enforceApproval(flowCtx, tool, toolContext, approvalConfig, stores) {
|
|
500
500
|
const { sessionId, userId } = resolveApprovalIdentity(toolContext.tryGetContext?.());
|
|
501
501
|
const currentContext = this.getCurrentContext(flowCtx);
|
|
502
|
-
const
|
|
502
|
+
const stored = [];
|
|
503
|
+
for (const store of stores) {
|
|
504
|
+
const records2 = await this.readApprovals(store, tool.fullName, sessionId, userId, currentContext);
|
|
505
|
+
stored.push(...records2.map((record) => ({ store, record })));
|
|
506
|
+
}
|
|
507
|
+
const records = stored.map(({ record }) => record);
|
|
503
508
|
if (records.some((record) => record.state === "denied" /* DENIED */)) {
|
|
504
509
|
throw new ApprovalRequiredError({
|
|
505
510
|
toolId: tool.fullName,
|
|
@@ -511,12 +516,13 @@ var ApprovalCheckPlugin = class extends DynamicPlugin {
|
|
|
511
516
|
return;
|
|
512
517
|
}
|
|
513
518
|
const approved = records.filter((record) => record.state === "approved" /* APPROVED */);
|
|
514
|
-
if (approvalConfig.alwaysPrompt) {
|
|
515
|
-
await this.handleApprovalRequired(flowCtx, approvalConfig, approved[0]);
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
519
|
const now = Date.now();
|
|
519
|
-
|
|
520
|
+
const usable = stored.filter(({ record }) => isApprovalUsable(record, approvalConfig, now));
|
|
521
|
+
if (approvalConfig.alwaysPrompt) {
|
|
522
|
+
for (const { store, record } of usable) {
|
|
523
|
+
if (await this.consumeApproval(store, record, sessionId, userId, currentContext)) return;
|
|
524
|
+
}
|
|
525
|
+
} else if (usable.length > 0) {
|
|
520
526
|
return;
|
|
521
527
|
}
|
|
522
528
|
await this.handleApprovalRequired(
|
|
@@ -525,6 +531,47 @@ var ApprovalCheckPlugin = class extends DynamicPlugin {
|
|
|
525
531
|
approved.find((record) => isApprovalExpired(record, approvalConfig, now)) ?? approved[0]
|
|
526
532
|
);
|
|
527
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* The stores of every approval gate that runs for the tool: this plugin's, and that of each other
|
|
536
|
+
* `ApprovalCheckPlugin` whose hook the scope runs for it (an `ApprovalPlugin` on the server and
|
|
537
|
+
* one on the tool's app, say). One store shared by two gates counts once.
|
|
538
|
+
*/
|
|
539
|
+
coveringStores(tool) {
|
|
540
|
+
const stores = /* @__PURE__ */ new Set([this.get(ApprovalStoreToken)]);
|
|
541
|
+
let scope;
|
|
542
|
+
try {
|
|
543
|
+
scope = this.get(ScopeEntry);
|
|
544
|
+
} catch {
|
|
545
|
+
scope = void 0;
|
|
546
|
+
}
|
|
547
|
+
if (!scope?.hooks) return [...stores];
|
|
548
|
+
for (const hook of scope.hooks.getFlowHooks("tools:call-tool")) {
|
|
549
|
+
const gate = hook.metadata.target;
|
|
550
|
+
if (gate instanceof ApprovalCheckPlugin && isEntryGatedBy(scope, { tool }, gate)) {
|
|
551
|
+
stores.add(gate.get(ApprovalStoreToken));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return [...stores];
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Use up an approval of an `alwaysPrompt` tool, reporting whether this call got it.
|
|
558
|
+
*
|
|
559
|
+
* `consumeApproval()` deletes exactly that record, and only one of two calls racing for it wins.
|
|
560
|
+
* A store without it has the caller's approvals of the tool revoked instead, which leaves none
|
|
561
|
+
* standing but cannot tell two concurrent calls apart.
|
|
562
|
+
*/
|
|
563
|
+
async consumeApproval(store, record, sessionId, userId, context) {
|
|
564
|
+
if (store.consumeApproval) {
|
|
565
|
+
return store.consumeApproval(record, sessionId, userId, context);
|
|
566
|
+
}
|
|
567
|
+
return store.revokeApproval({
|
|
568
|
+
toolId: record.toolId,
|
|
569
|
+
sessionId,
|
|
570
|
+
userId,
|
|
571
|
+
revokedBy: "system",
|
|
572
|
+
reason: "Used by a call of an alwaysPrompt tool"
|
|
573
|
+
});
|
|
574
|
+
}
|
|
528
575
|
/**
|
|
529
576
|
* The caller's approvals and denials of the tool, from the context it runs in too. Stores
|
|
530
577
|
* without `getApprovals()` give the one record `getApproval()` picks.
|
|
@@ -615,16 +662,19 @@ var ApprovalService = class {
|
|
|
615
662
|
/**
|
|
616
663
|
* Who a grant is recorded as when the call names no one: the signed-in user whose tool call made it.
|
|
617
664
|
* It used to be `'policy'`, so an audit trail showed every approval a user gave as a policy's.
|
|
618
|
-
* Without a signed-in user it is
|
|
665
|
+
* Without a signed-in user it is a user with no identifier.
|
|
666
|
+
*
|
|
667
|
+
* The method is `'implicit'`: a tool that grants through `this.approval` asked no one, so the grant
|
|
668
|
+
* is not an `'interactive'` answer. A tool that did ask passes `grantedBy: userGrantor(id)`.
|
|
619
669
|
*/
|
|
620
670
|
defaultGrantor() {
|
|
621
671
|
const userId = this.signedInUserId;
|
|
622
|
-
return userId ? userGrantor(userId) : { source: "user" };
|
|
672
|
+
return userId ? userGrantor(userId, void 0, { method: "implicit" }) : { source: "user", method: "implicit" };
|
|
623
673
|
}
|
|
624
674
|
/** Who a revocation is recorded as when the call names no one, by the same rule as a grant. */
|
|
625
675
|
defaultRevoker() {
|
|
626
676
|
const userId = this.signedInUserId;
|
|
627
|
-
return userId ?
|
|
677
|
+
return userId ? { source: "user", identifier: userId, method: "implicit" } : { source: "user", method: "implicit" };
|
|
628
678
|
}
|
|
629
679
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
630
680
|
// Query Methods
|
|
@@ -681,6 +731,13 @@ var ApprovalService = class {
|
|
|
681
731
|
(record) => record.sessionId === this.sessionId || this.userId !== void 0 && record.userId === this.userId
|
|
682
732
|
);
|
|
683
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* The caller's recent revocations of a tool, newest last: each is the approval that was revoked,
|
|
736
|
+
* with `revokedBy`, `revokedAt` and `revocationReason`. Empty when the store keeps no revocations.
|
|
737
|
+
*/
|
|
738
|
+
async getRevocations(toolId) {
|
|
739
|
+
return await this.store.getRevocations?.(toolId, this.sessionId, this.userId) ?? [];
|
|
740
|
+
}
|
|
684
741
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
685
742
|
// Grant Methods
|
|
686
743
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -948,14 +1005,22 @@ function createMemoryChallengeService(options = {}) {
|
|
|
948
1005
|
import { Provider as Provider3, ProviderScope as ProviderScope3 } from "@frontmcp/sdk";
|
|
949
1006
|
import {
|
|
950
1007
|
createMemoryStorage as createMemoryStorage2,
|
|
951
|
-
|
|
1008
|
+
createNamespacedStorage,
|
|
1009
|
+
createStorage as createStorage2,
|
|
1010
|
+
StorageNotSupportedError
|
|
952
1011
|
} from "@frontmcp/utils";
|
|
953
1012
|
function escapePattern(str) {
|
|
954
1013
|
return str.replace(/[*?[\]\\]/g, "\\$&");
|
|
955
1014
|
}
|
|
1015
|
+
function isSameRecord(a, b) {
|
|
1016
|
+
return a.toolId === b.toolId && a.state === b.state && a.scope === b.scope && a.grantedAt === b.grantedAt && a.sessionId === b.sessionId && a.userId === b.userId && a.context?.type === b.context?.type && a.context?.identifier === b.context?.identifier;
|
|
1017
|
+
}
|
|
1018
|
+
var REVOCATION_RETENTION_SECONDS = 24 * 60 * 60;
|
|
956
1019
|
var TIME_LIMITED_KEY_SEGMENT = `scope:${"time_limited" /* TIME_LIMITED */}`;
|
|
957
1020
|
var ApprovalStorageStore = class {
|
|
958
1021
|
storage;
|
|
1022
|
+
/** Revoked approvals live beside the live ones, in a namespace no live-approval query scans. */
|
|
1023
|
+
revocationStorage;
|
|
959
1024
|
options;
|
|
960
1025
|
cleanupInterval;
|
|
961
1026
|
initialized = false;
|
|
@@ -991,6 +1056,13 @@ var ApprovalStorageStore = class {
|
|
|
991
1056
|
}
|
|
992
1057
|
this.initialized = true;
|
|
993
1058
|
}
|
|
1059
|
+
get revocations() {
|
|
1060
|
+
this.revocationStorage ??= createNamespacedStorage(
|
|
1061
|
+
this.storage.root,
|
|
1062
|
+
`${this.storage.prefix.replace(/:$/, "")}-revoked`
|
|
1063
|
+
);
|
|
1064
|
+
return this.revocationStorage;
|
|
1065
|
+
}
|
|
994
1066
|
ensureInitialized() {
|
|
995
1067
|
if (!this.initialized) {
|
|
996
1068
|
throw new Error("ApprovalStorageStore not initialized. Call initialize() first.");
|
|
@@ -1152,27 +1224,68 @@ var ApprovalStorageStore = class {
|
|
|
1152
1224
|
const keys = await this.storage.keys(`${escapePattern(toolId)}:*`);
|
|
1153
1225
|
const values = await this.storage.mget(keys);
|
|
1154
1226
|
const keysToDelete = [];
|
|
1227
|
+
const revoked = /* @__PURE__ */ new Map();
|
|
1155
1228
|
for (let i = 0; i < keys.length; i++) {
|
|
1156
1229
|
const record = this.parseRecord(values[i]);
|
|
1157
1230
|
if (!record || record.toolId !== toolId || record.state === "denied" /* DENIED */) continue;
|
|
1231
|
+
const matches = keysToDelete.length;
|
|
1158
1232
|
if (context) {
|
|
1159
1233
|
const sameSession = sessionId !== void 0 && record.sessionId === sessionId;
|
|
1160
1234
|
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.
|
|
1161
1235
|
(!userId || record.userId === userId || sameSession && record.userId === void 0)) {
|
|
1162
1236
|
keysToDelete.push(keys[i]);
|
|
1163
1237
|
}
|
|
1164
|
-
|
|
1165
|
-
}
|
|
1166
|
-
if (sessionId && record.sessionId === sessionId || userId && record.userId === userId) {
|
|
1238
|
+
} else if (sessionId && record.sessionId === sessionId || userId && record.userId === userId) {
|
|
1167
1239
|
keysToDelete.push(keys[i]);
|
|
1168
1240
|
}
|
|
1241
|
+
if (keysToDelete.length > matches) revoked.set(keys[i], record);
|
|
1169
1242
|
}
|
|
1170
1243
|
if (keysToDelete.length === 0) {
|
|
1171
1244
|
return false;
|
|
1172
1245
|
}
|
|
1246
|
+
const revokedAt = Date.now();
|
|
1247
|
+
const revokedBy = normalizeRevoker(options.revokedBy);
|
|
1248
|
+
for (const [key, record] of revoked) {
|
|
1249
|
+
const entry = { ...record, revokedAt, revokedBy, revocationReason: options.reason };
|
|
1250
|
+
await this.revocations.set(key, JSON.stringify(entry), { ttlSeconds: REVOCATION_RETENTION_SECONDS });
|
|
1251
|
+
}
|
|
1173
1252
|
await this.storage.mdelete(keysToDelete);
|
|
1174
1253
|
return true;
|
|
1175
1254
|
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Deletes the key that holds `record`, if it still does, and reports whether this call deleted
|
|
1257
|
+
* it. Two calls racing for one approval both find it, but only one deletes it, and a denial or
|
|
1258
|
+
* approval written to the key after it was read is kept.
|
|
1259
|
+
*/
|
|
1260
|
+
async consumeApproval(record, sessionId, userId, context) {
|
|
1261
|
+
this.ensureInitialized();
|
|
1262
|
+
for (const key of this.callerKeys(record.toolId, sessionId, userId, context)) {
|
|
1263
|
+
const storedValue = await this.storage.get(key);
|
|
1264
|
+
const stored = this.parseRecord(storedValue);
|
|
1265
|
+
if (storedValue !== null && stored && isSameRecord(stored, record)) {
|
|
1266
|
+
return this.deleteIfUnchanged(key, storedValue);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
/** Deletes `key` only while it still holds `storedValue`, in one step where the backend allows. */
|
|
1272
|
+
async deleteIfUnchanged(key, storedValue) {
|
|
1273
|
+
try {
|
|
1274
|
+
return await this.storage.deleteIfEquals(key, storedValue);
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
if (!(error instanceof StorageNotSupportedError)) throw error;
|
|
1277
|
+
return this.storage.delete(key);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async getRevocations(toolId, sessionId, userId, context) {
|
|
1281
|
+
this.ensureInitialized();
|
|
1282
|
+
const records = [];
|
|
1283
|
+
for (const key of this.callerKeys(toolId, sessionId, userId, context)) {
|
|
1284
|
+
const record = this.parseRecord(await this.revocations.get(key));
|
|
1285
|
+
if (record && record.toolId === toolId) records.push(record);
|
|
1286
|
+
}
|
|
1287
|
+
return records.sort((a, b) => (a.revokedAt ?? 0) - (b.revokedAt ?? 0));
|
|
1288
|
+
}
|
|
1176
1289
|
async isApproved(toolId, sessionId, userId, context) {
|
|
1177
1290
|
const approval = await this.getApproval(toolId, sessionId, userId, context);
|
|
1178
1291
|
return approval?.state === "approved" /* APPROVED */;
|
|
@@ -1248,6 +1361,7 @@ var ApprovalStorageStore = class {
|
|
|
1248
1361
|
if (this.ownedStorage && this.storage) {
|
|
1249
1362
|
await this.storage.root.disconnect();
|
|
1250
1363
|
}
|
|
1364
|
+
this.revocationStorage = void 0;
|
|
1251
1365
|
this.initialized = false;
|
|
1252
1366
|
}
|
|
1253
1367
|
};
|
|
@@ -1329,7 +1443,7 @@ __publicField(ApprovalPlugin, "dynamicProviders", (options) => {
|
|
|
1329
1443
|
name: "approval:service",
|
|
1330
1444
|
provide: ApprovalServiceToken,
|
|
1331
1445
|
scope: ProviderScope4.CONTEXT,
|
|
1332
|
-
inject: () => [ApprovalStoreToken, FRONTMCP_CONTEXT,
|
|
1446
|
+
inject: () => [ApprovalStoreToken, FRONTMCP_CONTEXT, ScopeEntry2],
|
|
1333
1447
|
useFactory: (store, ctx, scope) => {
|
|
1334
1448
|
const { sessionId, userId } = resolveApprovalIdentity(ctx);
|
|
1335
1449
|
const requirementOf = (toolId) => {
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-approval",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Approval plugin for FrontMCP - tool authorization workflow with PKCE webhook security",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@frontmcp/sdk": "1.
|
|
53
|
-
"@frontmcp/utils": "1.
|
|
52
|
+
"@frontmcp/sdk": "1.9.0",
|
|
53
|
+
"@frontmcp/utils": "1.9.0",
|
|
54
54
|
"ioredis": "^5.8.0",
|
|
55
55
|
"reflect-metadata": "^0.2.2"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@frontmcp/lazy-zod": "1.
|
|
58
|
+
"@frontmcp/lazy-zod": "1.9.0",
|
|
59
59
|
"@vercel/kv": "^2.0.0 || ^3.0.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
@@ -17,9 +17,28 @@ export default class ApprovalCheckPlugin extends DynamicPlugin<Record<string, ne
|
|
|
17
17
|
* Runs for the tools of the app the plugin is installed on and, `appliesTo: 'uncovered-apps'`,
|
|
18
18
|
* for the tools of any app with no approval gate of its own, so an `approval` tool is never left
|
|
19
19
|
* ungated because the plugin sits on a different app.
|
|
20
|
+
*
|
|
21
|
+
* A tool more than one gate covers (an `ApprovalPlugin` on the server and one on the tool's app)
|
|
22
|
+
* is decided once, over all their stores: an approval in any of them lets it run, a denial in any
|
|
23
|
+
* of them refuses it. It used to need an approval in each store (#678), so a grant through
|
|
24
|
+
* `this.approval`, which writes to one of them, never let it run.
|
|
20
25
|
*/
|
|
21
26
|
checkApproval(flowCtx: FlowCtxOf<'tools:call-tool'>): Promise<void>;
|
|
22
27
|
private enforceApproval;
|
|
28
|
+
/**
|
|
29
|
+
* The stores of every approval gate that runs for the tool: this plugin's, and that of each other
|
|
30
|
+
* `ApprovalCheckPlugin` whose hook the scope runs for it (an `ApprovalPlugin` on the server and
|
|
31
|
+
* one on the tool's app, say). One store shared by two gates counts once.
|
|
32
|
+
*/
|
|
33
|
+
private coveringStores;
|
|
34
|
+
/**
|
|
35
|
+
* Use up an approval of an `alwaysPrompt` tool, reporting whether this call got it.
|
|
36
|
+
*
|
|
37
|
+
* `consumeApproval()` deletes exactly that record, and only one of two calls racing for it wins.
|
|
38
|
+
* A store without it has the caller's approvals of the tool revoked instead, which leaves none
|
|
39
|
+
* standing but cannot tell two concurrent calls apart.
|
|
40
|
+
*/
|
|
41
|
+
private consumeApproval;
|
|
23
42
|
/**
|
|
24
43
|
* The caller's approvals and denials of the tool, from the context it runs in too. Stores
|
|
25
44
|
* without `getApprovals()` give the one record `getApproval()` picks.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-check.hook.d.ts","sourceRoot":"","sources":["../../src/hooks/approval-check.hook.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"approval-check.hook.d.ts","sourceRoot":"","sources":["../../src/hooks/approval-check.hook.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAgD,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAwB5G;;;;;GAKG;AAOH,MAAM,CAAC,OAAO,OAAO,mBAAoB,SAAQ,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACnF;;;;;;;;;;;OAWG;IAEG,aAAa,CAAC,OAAO,EAAE,SAAS,CAAC,iBAAiB,CAAC;YAwB3C,eAAe;IAqD7B;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAoBtB;;;;;;OAMG;YACW,eAAe;IAmB7B;;;OAGG;YACW,aAAa;IAc3B;;;;;;;;OAQG;IACH,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,oBAAoB;YAUd,sBAAsB;CAsBrC"}
|
package/index.js
CHANGED
|
@@ -526,7 +526,7 @@ function isApiGrantor(grantor) {
|
|
|
526
526
|
}
|
|
527
527
|
|
|
528
528
|
// plugins/plugin-approval/src/hooks/approval-check.hook.ts
|
|
529
|
-
var
|
|
529
|
+
var decidedCalls = /* @__PURE__ */ new WeakSet();
|
|
530
530
|
var ApprovalCheckPlugin = class extends import_sdk2.DynamicPlugin {
|
|
531
531
|
async checkApproval(flowCtx) {
|
|
532
532
|
const { tool, toolContext } = flowCtx.state;
|
|
@@ -541,17 +541,19 @@ var ApprovalCheckPlugin = class extends import_sdk2.DynamicPlugin {
|
|
|
541
541
|
if (approvalConfig.skipApproval) {
|
|
542
542
|
return;
|
|
543
543
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
passedApprovalStores.set(toolContext, passedStores);
|
|
550
|
-
}
|
|
551
|
-
async enforceApproval(flowCtx, tool, toolContext, approvalConfig, approvalStore) {
|
|
544
|
+
if (decidedCalls.has(toolContext)) return;
|
|
545
|
+
await this.enforceApproval(flowCtx, tool, toolContext, approvalConfig, this.coveringStores(tool));
|
|
546
|
+
decidedCalls.add(toolContext);
|
|
547
|
+
}
|
|
548
|
+
async enforceApproval(flowCtx, tool, toolContext, approvalConfig, stores) {
|
|
552
549
|
const { sessionId, userId } = resolveApprovalIdentity(toolContext.tryGetContext?.());
|
|
553
550
|
const currentContext = this.getCurrentContext(flowCtx);
|
|
554
|
-
const
|
|
551
|
+
const stored = [];
|
|
552
|
+
for (const store of stores) {
|
|
553
|
+
const records2 = await this.readApprovals(store, tool.fullName, sessionId, userId, currentContext);
|
|
554
|
+
stored.push(...records2.map((record) => ({ store, record })));
|
|
555
|
+
}
|
|
556
|
+
const records = stored.map(({ record }) => record);
|
|
555
557
|
if (records.some((record) => record.state === "denied" /* DENIED */)) {
|
|
556
558
|
throw new ApprovalRequiredError({
|
|
557
559
|
toolId: tool.fullName,
|
|
@@ -563,12 +565,13 @@ var ApprovalCheckPlugin = class extends import_sdk2.DynamicPlugin {
|
|
|
563
565
|
return;
|
|
564
566
|
}
|
|
565
567
|
const approved = records.filter((record) => record.state === "approved" /* APPROVED */);
|
|
566
|
-
if (approvalConfig.alwaysPrompt) {
|
|
567
|
-
await this.handleApprovalRequired(flowCtx, approvalConfig, approved[0]);
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
568
|
const now = Date.now();
|
|
571
|
-
|
|
569
|
+
const usable = stored.filter(({ record }) => isApprovalUsable(record, approvalConfig, now));
|
|
570
|
+
if (approvalConfig.alwaysPrompt) {
|
|
571
|
+
for (const { store, record } of usable) {
|
|
572
|
+
if (await this.consumeApproval(store, record, sessionId, userId, currentContext)) return;
|
|
573
|
+
}
|
|
574
|
+
} else if (usable.length > 0) {
|
|
572
575
|
return;
|
|
573
576
|
}
|
|
574
577
|
await this.handleApprovalRequired(
|
|
@@ -577,6 +580,47 @@ var ApprovalCheckPlugin = class extends import_sdk2.DynamicPlugin {
|
|
|
577
580
|
approved.find((record) => isApprovalExpired(record, approvalConfig, now)) ?? approved[0]
|
|
578
581
|
);
|
|
579
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* The stores of every approval gate that runs for the tool: this plugin's, and that of each other
|
|
585
|
+
* `ApprovalCheckPlugin` whose hook the scope runs for it (an `ApprovalPlugin` on the server and
|
|
586
|
+
* one on the tool's app, say). One store shared by two gates counts once.
|
|
587
|
+
*/
|
|
588
|
+
coveringStores(tool) {
|
|
589
|
+
const stores = /* @__PURE__ */ new Set([this.get(ApprovalStoreToken)]);
|
|
590
|
+
let scope;
|
|
591
|
+
try {
|
|
592
|
+
scope = this.get(import_sdk2.ScopeEntry);
|
|
593
|
+
} catch {
|
|
594
|
+
scope = void 0;
|
|
595
|
+
}
|
|
596
|
+
if (!scope?.hooks) return [...stores];
|
|
597
|
+
for (const hook of scope.hooks.getFlowHooks("tools:call-tool")) {
|
|
598
|
+
const gate = hook.metadata.target;
|
|
599
|
+
if (gate instanceof ApprovalCheckPlugin && (0, import_sdk2.isEntryGatedBy)(scope, { tool }, gate)) {
|
|
600
|
+
stores.add(gate.get(ApprovalStoreToken));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return [...stores];
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Use up an approval of an `alwaysPrompt` tool, reporting whether this call got it.
|
|
607
|
+
*
|
|
608
|
+
* `consumeApproval()` deletes exactly that record, and only one of two calls racing for it wins.
|
|
609
|
+
* A store without it has the caller's approvals of the tool revoked instead, which leaves none
|
|
610
|
+
* standing but cannot tell two concurrent calls apart.
|
|
611
|
+
*/
|
|
612
|
+
async consumeApproval(store, record, sessionId, userId, context) {
|
|
613
|
+
if (store.consumeApproval) {
|
|
614
|
+
return store.consumeApproval(record, sessionId, userId, context);
|
|
615
|
+
}
|
|
616
|
+
return store.revokeApproval({
|
|
617
|
+
toolId: record.toolId,
|
|
618
|
+
sessionId,
|
|
619
|
+
userId,
|
|
620
|
+
revokedBy: "system",
|
|
621
|
+
reason: "Used by a call of an alwaysPrompt tool"
|
|
622
|
+
});
|
|
623
|
+
}
|
|
580
624
|
/**
|
|
581
625
|
* The caller's approvals and denials of the tool, from the context it runs in too. Stores
|
|
582
626
|
* without `getApprovals()` give the one record `getApproval()` picks.
|
|
@@ -667,16 +711,19 @@ var ApprovalService = class {
|
|
|
667
711
|
/**
|
|
668
712
|
* Who a grant is recorded as when the call names no one: the signed-in user whose tool call made it.
|
|
669
713
|
* It used to be `'policy'`, so an audit trail showed every approval a user gave as a policy's.
|
|
670
|
-
* Without a signed-in user it is
|
|
714
|
+
* Without a signed-in user it is a user with no identifier.
|
|
715
|
+
*
|
|
716
|
+
* The method is `'implicit'`: a tool that grants through `this.approval` asked no one, so the grant
|
|
717
|
+
* is not an `'interactive'` answer. A tool that did ask passes `grantedBy: userGrantor(id)`.
|
|
671
718
|
*/
|
|
672
719
|
defaultGrantor() {
|
|
673
720
|
const userId = this.signedInUserId;
|
|
674
|
-
return userId ? userGrantor(userId) : { source: "user" };
|
|
721
|
+
return userId ? userGrantor(userId, void 0, { method: "implicit" }) : { source: "user", method: "implicit" };
|
|
675
722
|
}
|
|
676
723
|
/** Who a revocation is recorded as when the call names no one, by the same rule as a grant. */
|
|
677
724
|
defaultRevoker() {
|
|
678
725
|
const userId = this.signedInUserId;
|
|
679
|
-
return userId ?
|
|
726
|
+
return userId ? { source: "user", identifier: userId, method: "implicit" } : { source: "user", method: "implicit" };
|
|
680
727
|
}
|
|
681
728
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
682
729
|
// Query Methods
|
|
@@ -733,6 +780,13 @@ var ApprovalService = class {
|
|
|
733
780
|
(record) => record.sessionId === this.sessionId || this.userId !== void 0 && record.userId === this.userId
|
|
734
781
|
);
|
|
735
782
|
}
|
|
783
|
+
/**
|
|
784
|
+
* The caller's recent revocations of a tool, newest last: each is the approval that was revoked,
|
|
785
|
+
* with `revokedBy`, `revokedAt` and `revocationReason`. Empty when the store keeps no revocations.
|
|
786
|
+
*/
|
|
787
|
+
async getRevocations(toolId) {
|
|
788
|
+
return await this.store.getRevocations?.(toolId, this.sessionId, this.userId) ?? [];
|
|
789
|
+
}
|
|
736
790
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
737
791
|
// Grant Methods
|
|
738
792
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -997,9 +1051,15 @@ var import_utils3 = require("@frontmcp/utils");
|
|
|
997
1051
|
function escapePattern(str) {
|
|
998
1052
|
return str.replace(/[*?[\]\\]/g, "\\$&");
|
|
999
1053
|
}
|
|
1054
|
+
function isSameRecord(a, b) {
|
|
1055
|
+
return a.toolId === b.toolId && a.state === b.state && a.scope === b.scope && a.grantedAt === b.grantedAt && a.sessionId === b.sessionId && a.userId === b.userId && a.context?.type === b.context?.type && a.context?.identifier === b.context?.identifier;
|
|
1056
|
+
}
|
|
1057
|
+
var REVOCATION_RETENTION_SECONDS = 24 * 60 * 60;
|
|
1000
1058
|
var TIME_LIMITED_KEY_SEGMENT = `scope:${"time_limited" /* TIME_LIMITED */}`;
|
|
1001
1059
|
var ApprovalStorageStore = class {
|
|
1002
1060
|
storage;
|
|
1061
|
+
/** Revoked approvals live beside the live ones, in a namespace no live-approval query scans. */
|
|
1062
|
+
revocationStorage;
|
|
1003
1063
|
options;
|
|
1004
1064
|
cleanupInterval;
|
|
1005
1065
|
initialized = false;
|
|
@@ -1035,6 +1095,13 @@ var ApprovalStorageStore = class {
|
|
|
1035
1095
|
}
|
|
1036
1096
|
this.initialized = true;
|
|
1037
1097
|
}
|
|
1098
|
+
get revocations() {
|
|
1099
|
+
this.revocationStorage ??= (0, import_utils3.createNamespacedStorage)(
|
|
1100
|
+
this.storage.root,
|
|
1101
|
+
`${this.storage.prefix.replace(/:$/, "")}-revoked`
|
|
1102
|
+
);
|
|
1103
|
+
return this.revocationStorage;
|
|
1104
|
+
}
|
|
1038
1105
|
ensureInitialized() {
|
|
1039
1106
|
if (!this.initialized) {
|
|
1040
1107
|
throw new Error("ApprovalStorageStore not initialized. Call initialize() first.");
|
|
@@ -1196,27 +1263,68 @@ var ApprovalStorageStore = class {
|
|
|
1196
1263
|
const keys = await this.storage.keys(`${escapePattern(toolId)}:*`);
|
|
1197
1264
|
const values = await this.storage.mget(keys);
|
|
1198
1265
|
const keysToDelete = [];
|
|
1266
|
+
const revoked = /* @__PURE__ */ new Map();
|
|
1199
1267
|
for (let i = 0; i < keys.length; i++) {
|
|
1200
1268
|
const record = this.parseRecord(values[i]);
|
|
1201
1269
|
if (!record || record.toolId !== toolId || record.state === "denied" /* DENIED */) continue;
|
|
1270
|
+
const matches = keysToDelete.length;
|
|
1202
1271
|
if (context) {
|
|
1203
1272
|
const sameSession = sessionId !== void 0 && record.sessionId === sessionId;
|
|
1204
1273
|
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.
|
|
1205
1274
|
(!userId || record.userId === userId || sameSession && record.userId === void 0)) {
|
|
1206
1275
|
keysToDelete.push(keys[i]);
|
|
1207
1276
|
}
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
if (sessionId && record.sessionId === sessionId || userId && record.userId === userId) {
|
|
1277
|
+
} else if (sessionId && record.sessionId === sessionId || userId && record.userId === userId) {
|
|
1211
1278
|
keysToDelete.push(keys[i]);
|
|
1212
1279
|
}
|
|
1280
|
+
if (keysToDelete.length > matches) revoked.set(keys[i], record);
|
|
1213
1281
|
}
|
|
1214
1282
|
if (keysToDelete.length === 0) {
|
|
1215
1283
|
return false;
|
|
1216
1284
|
}
|
|
1285
|
+
const revokedAt = Date.now();
|
|
1286
|
+
const revokedBy = normalizeRevoker(options.revokedBy);
|
|
1287
|
+
for (const [key, record] of revoked) {
|
|
1288
|
+
const entry = { ...record, revokedAt, revokedBy, revocationReason: options.reason };
|
|
1289
|
+
await this.revocations.set(key, JSON.stringify(entry), { ttlSeconds: REVOCATION_RETENTION_SECONDS });
|
|
1290
|
+
}
|
|
1217
1291
|
await this.storage.mdelete(keysToDelete);
|
|
1218
1292
|
return true;
|
|
1219
1293
|
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Deletes the key that holds `record`, if it still does, and reports whether this call deleted
|
|
1296
|
+
* it. Two calls racing for one approval both find it, but only one deletes it, and a denial or
|
|
1297
|
+
* approval written to the key after it was read is kept.
|
|
1298
|
+
*/
|
|
1299
|
+
async consumeApproval(record, sessionId, userId, context) {
|
|
1300
|
+
this.ensureInitialized();
|
|
1301
|
+
for (const key of this.callerKeys(record.toolId, sessionId, userId, context)) {
|
|
1302
|
+
const storedValue = await this.storage.get(key);
|
|
1303
|
+
const stored = this.parseRecord(storedValue);
|
|
1304
|
+
if (storedValue !== null && stored && isSameRecord(stored, record)) {
|
|
1305
|
+
return this.deleteIfUnchanged(key, storedValue);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
/** Deletes `key` only while it still holds `storedValue`, in one step where the backend allows. */
|
|
1311
|
+
async deleteIfUnchanged(key, storedValue) {
|
|
1312
|
+
try {
|
|
1313
|
+
return await this.storage.deleteIfEquals(key, storedValue);
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
if (!(error instanceof import_utils3.StorageNotSupportedError)) throw error;
|
|
1316
|
+
return this.storage.delete(key);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
async getRevocations(toolId, sessionId, userId, context) {
|
|
1320
|
+
this.ensureInitialized();
|
|
1321
|
+
const records = [];
|
|
1322
|
+
for (const key of this.callerKeys(toolId, sessionId, userId, context)) {
|
|
1323
|
+
const record = this.parseRecord(await this.revocations.get(key));
|
|
1324
|
+
if (record && record.toolId === toolId) records.push(record);
|
|
1325
|
+
}
|
|
1326
|
+
return records.sort((a, b) => (a.revokedAt ?? 0) - (b.revokedAt ?? 0));
|
|
1327
|
+
}
|
|
1220
1328
|
async isApproved(toolId, sessionId, userId, context) {
|
|
1221
1329
|
const approval = await this.getApproval(toolId, sessionId, userId, context);
|
|
1222
1330
|
return approval?.state === "approved" /* APPROVED */;
|
|
@@ -1292,6 +1400,7 @@ var ApprovalStorageStore = class {
|
|
|
1292
1400
|
if (this.ownedStorage && this.storage) {
|
|
1293
1401
|
await this.storage.root.disconnect();
|
|
1294
1402
|
}
|
|
1403
|
+
this.revocationStorage = void 0;
|
|
1295
1404
|
this.initialized = false;
|
|
1296
1405
|
}
|
|
1297
1406
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-approval",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Approval plugin for FrontMCP - tool authorization workflow with PKCE webhook security",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@frontmcp/sdk": "1.
|
|
53
|
-
"@frontmcp/utils": "1.
|
|
52
|
+
"@frontmcp/sdk": "1.9.0",
|
|
53
|
+
"@frontmcp/utils": "1.9.0",
|
|
54
54
|
"ioredis": "^5.8.0",
|
|
55
55
|
"reflect-metadata": "^0.2.2"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@frontmcp/lazy-zod": "1.
|
|
58
|
+
"@frontmcp/lazy-zod": "1.9.0",
|
|
59
59
|
"@vercel/kv": "^2.0.0 || ^3.0.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
@@ -46,7 +46,10 @@ export declare class ApprovalService {
|
|
|
46
46
|
/**
|
|
47
47
|
* Who a grant is recorded as when the call names no one: the signed-in user whose tool call made it.
|
|
48
48
|
* It used to be `'policy'`, so an audit trail showed every approval a user gave as a policy's.
|
|
49
|
-
* Without a signed-in user it is
|
|
49
|
+
* Without a signed-in user it is a user with no identifier.
|
|
50
|
+
*
|
|
51
|
+
* The method is `'implicit'`: a tool that grants through `this.approval` asked no one, so the grant
|
|
52
|
+
* is not an `'interactive'` answer. A tool that did ask passes `grantedBy: userGrantor(id)`.
|
|
50
53
|
*/
|
|
51
54
|
private defaultGrantor;
|
|
52
55
|
/** Who a revocation is recorded as when the call names no one, by the same rule as a grant. */
|
|
@@ -73,6 +76,11 @@ export declare class ApprovalService {
|
|
|
73
76
|
* another caller's records. Read the store directly for administrative queries.
|
|
74
77
|
*/
|
|
75
78
|
queryApprovals(query: Partial<ApprovalQuery>): Promise<ApprovalRecord[]>;
|
|
79
|
+
/**
|
|
80
|
+
* The caller's recent revocations of a tool, newest last: each is the approval that was revoked,
|
|
81
|
+
* with `revokedBy`, `revokedAt` and `revocationReason`. Empty when the store keeps no revocations.
|
|
82
|
+
*/
|
|
83
|
+
getRevocations(toolId: string): Promise<ApprovalRecord[]>;
|
|
76
84
|
/**
|
|
77
85
|
* Grant session-scoped approval for a tool.
|
|
78
86
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval.service.d.ts","sourceRoot":"","sources":["../../src/services/approval.service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC7B,MAAM,UAAU,CAAC;AAElB;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,uBAAuB,GAAG,SAAS,CAAC;AAEhG;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,8GAA8G;IAC9G,SAAS,CAAC,EAAE,eAAe,GAAG,kBAAkB,CAAC;IACjD,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,8GAA8G;IAC9G,SAAS,CAAC,EAAE,eAAe,GAAG,oBAAoB,CAAC;IACnD,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,qBAKa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAHb,KAAK,EAAE,aAAa,EACpB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,aAAa,GAAE,yBAA2C;IAG7E,kGAAkG;IAClG,OAAO,CAAC,UAAU;IAIlB,qGAAqG;IACrG,OAAO,KAAK,cAAc,GAEzB;IAED
|
|
1
|
+
{"version":3,"file":"approval.service.d.ts","sourceRoot":"","sources":["../../src/services/approval.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -37,6 +37,8 @@ export interface ApprovalStorageStoreOptions {
|
|
|
37
37
|
*/
|
|
38
38
|
export declare class ApprovalStorageStore implements ApprovalStore {
|
|
39
39
|
private storage;
|
|
40
|
+
/** Revoked approvals live beside the live ones, in a namespace no live-approval query scans. */
|
|
41
|
+
private revocationStorage?;
|
|
40
42
|
private readonly options;
|
|
41
43
|
private cleanupInterval?;
|
|
42
44
|
private initialized;
|
|
@@ -46,6 +48,7 @@ export declare class ApprovalStorageStore implements ApprovalStore {
|
|
|
46
48
|
* Initialize the storage connection.
|
|
47
49
|
*/
|
|
48
50
|
initialize(): Promise<void>;
|
|
51
|
+
private get revocations();
|
|
49
52
|
private ensureInitialized;
|
|
50
53
|
private buildKey;
|
|
51
54
|
/**
|
|
@@ -85,6 +88,15 @@ export declare class ApprovalStorageStore implements ApprovalStore {
|
|
|
85
88
|
* (session, user, time-limited and context approvals alike). Recorded denials are kept.
|
|
86
89
|
*/
|
|
87
90
|
revokeApproval(options: RevokeApprovalOptions): Promise<boolean>;
|
|
91
|
+
/**
|
|
92
|
+
* Deletes the key that holds `record`, if it still does, and reports whether this call deleted
|
|
93
|
+
* it. Two calls racing for one approval both find it, but only one deletes it, and a denial or
|
|
94
|
+
* approval written to the key after it was read is kept.
|
|
95
|
+
*/
|
|
96
|
+
consumeApproval(record: ApprovalRecord, sessionId: string, userId?: string, context?: ApprovalContext): Promise<boolean>;
|
|
97
|
+
/** Deletes `key` only while it still holds `storedValue`, in one step where the backend allows. */
|
|
98
|
+
private deleteIfUnchanged;
|
|
99
|
+
getRevocations(toolId: string, sessionId: string, userId?: string, context?: ApprovalContext): Promise<ApprovalRecord[]>;
|
|
88
100
|
isApproved(toolId: string, sessionId: string, userId?: string, context?: ApprovalContext): Promise<boolean>;
|
|
89
101
|
clearSessionApprovals(sessionId: string): Promise<number>;
|
|
90
102
|
clearExpiredApprovals(): Promise<number>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-storage.store.d.ts","sourceRoot":"","sources":["../../src/stores/approval-storage.store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,
|
|
1
|
+
{"version":3,"file":"approval-storage.store.d.ts","sourceRoot":"","sources":["../../src/stores/approval-storage.store.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module @frontmcp/plugin-approval
|
|
5
5
|
*/
|
|
6
|
-
import type {
|
|
6
|
+
import type { ApprovalContext, ApprovalGrantor, ApprovalRecord, ApprovalRevoker, ApprovalScope, ApprovalSourceType, ApprovalState, RevocationSourceType } from '../types';
|
|
7
7
|
/**
|
|
8
8
|
* Query options for finding approvals.
|
|
9
9
|
*/
|
|
@@ -101,6 +101,20 @@ export interface ApprovalStore {
|
|
|
101
101
|
* (only the given context's, when `context` is set). Recorded denials are kept.
|
|
102
102
|
*/
|
|
103
103
|
revokeApproval(options: RevokeApprovalOptions): Promise<boolean>;
|
|
104
|
+
/**
|
|
105
|
+
* Use up one approval: delete the stored record `record` (one `getApprovals()` returned for this
|
|
106
|
+
* caller) and resolve `true` only for the call that deleted it, so two calls racing for one
|
|
107
|
+
* approval cannot both get it. A record written in its place since it was read (a denial, a new
|
|
108
|
+
* approval) must be kept, so the delete has to compare and delete in one step. The approval gate uses it for `alwaysPrompt` tools, where each
|
|
109
|
+
* approval admits a single call. Optional: for a store without it the gate revokes the caller's
|
|
110
|
+
* approvals of the tool instead.
|
|
111
|
+
*/
|
|
112
|
+
consumeApproval?(record: ApprovalRecord, sessionId: string, userId?: string, context?: ApprovalContext): Promise<boolean>;
|
|
113
|
+
/**
|
|
114
|
+
* The caller's recent revocations of a tool: each is the approval that was revoked, with `revokedBy`,
|
|
115
|
+
* `revokedAt` and `revocationReason`. Optional: a store that keeps no revocations leaves it out.
|
|
116
|
+
*/
|
|
117
|
+
getRevocations?(toolId: string, sessionId: string, userId?: string, context?: ApprovalContext): Promise<ApprovalRecord[]>;
|
|
104
118
|
/**
|
|
105
119
|
* Check if a tool is approved.
|
|
106
120
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-store.interface.d.ts","sourceRoot":"","sources":["../../src/stores/approval-store.interface.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"approval-store.interface.d.ts","sourceRoot":"","sources":["../../src/stores/approval-store.interface.ts"],"names":[],"mappings":"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"}
|