@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.
@@ -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;
@@ -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 passedApprovalStores = /* @__PURE__ */ new WeakMap();
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
- const approvalStore = this.get(ApprovalStoreToken);
493
- const passedStores = passedApprovalStores.get(toolContext) ?? /* @__PURE__ */ new WeakSet();
494
- if (passedStores.has(approvalStore)) return;
495
- await this.enforceApproval(flowCtx, tool, toolContext, approvalConfig, approvalStore);
496
- passedStores.add(approvalStore);
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 records = await this.readApprovals(approvalStore, tool.fullName, sessionId, userId, currentContext);
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
- if (approved.some((record) => isApprovalUsable(record, approvalConfig, now))) {
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 the store's default, a user with no identifier.
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 ? userRevoker(userId) : { source: "user" };
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
- createStorage as createStorage2
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
- continue;
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, ScopeEntry],
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.8.6",
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.8.6",
53
- "@frontmcp/utils": "1.8.6",
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.8.6",
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,EAAoB,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAmBhF;;;;;GAKG;AAOH,MAAM,CAAC,OAAO,OAAO,mBAAoB,SAAQ,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACnF;;;;;;OAMG;IAEG,aAAa,CAAC,OAAO,EAAE,SAAS,CAAC,iBAAiB,CAAC;YA2B3C,eAAe;IA6C7B;;;OAGG;YACW,aAAa;IAc3B;;;;;;;;OAQG;IACH,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,oBAAoB;YAUd,sBAAsB;CAsBrC"}
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 passedApprovalStores = /* @__PURE__ */ new WeakMap();
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
- const approvalStore = this.get(ApprovalStoreToken);
545
- const passedStores = passedApprovalStores.get(toolContext) ?? /* @__PURE__ */ new WeakSet();
546
- if (passedStores.has(approvalStore)) return;
547
- await this.enforceApproval(flowCtx, tool, toolContext, approvalConfig, approvalStore);
548
- passedStores.add(approvalStore);
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 records = await this.readApprovals(approvalStore, tool.fullName, sessionId, userId, currentContext);
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
- if (approved.some((record) => isApprovalUsable(record, approvalConfig, now))) {
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 the store's default, a user with no identifier.
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 ? userRevoker(userId) : { source: "user" };
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
- continue;
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.8.6",
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.8.6",
53
- "@frontmcp/utils": "1.8.6",
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.8.6",
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 the store's default, a user with no identifier.
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":"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"}
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,EAGL,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AACnG,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,4BAA4B,CAAC;AAqBpC;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB;;OAEG;IACH,eAAe,CAAC,EAAE,WAAW,GAAG,iBAAiB,CAAC;IAElD;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAMD;;;GAGG;AACH,qBAKa,oBAAqB,YAAW,aAAa;IACxD,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAEtB;IACF,OAAO,CAAC,eAAe,CAAC,CAAiB;IACzC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;gBAEjB,OAAO,GAAE,2BAAgC;IASrD;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBjC,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,QAAQ;IAQhB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAItB,4CAA4C;IAC5C,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,SAAS;IAIjB;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU;IAgBlB;;;OAGG;IACG,YAAY,CAChB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,EAAE,CAAC;IAa5B;;;OAGG;IACG,WAAW,CACf,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAKhC,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAwC/D,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,cAAc,CAAC;IAkC3E;;;;OAIG;IACG,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,OAAO,CAAC;IAuChE,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IAK3G,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBzD,qBAAqB,IAAI,OAAO,CAAC,MAAM,CAAC;IAuBxC,QAAQ,IAAI,OAAO,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;QACvC,OAAO,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;KACxC,CAAC;IAsCI,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAY7B;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,IAAI,CAAC,2BAA2B,EAAE,SAAS,GAAG,iBAAiB,CAAM,GAC7E,oBAAoB,CAMtB"}
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 { ApprovalScope, ApprovalState, ApprovalContext, ApprovalGrantor, ApprovalRevoker, ApprovalRecord, ApprovalSourceType, RevocationSourceType } from '../types';
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":"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"}
1
+ {"version":3,"file":"approval-store.interface.d.ts","sourceRoot":"","sources":["../../src/stores/approval-store.interface.ts"],"names":[],"mappings":"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"}