@frontmcp/plugin-approval 1.8.7 → 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.
@@ -959,11 +1006,15 @@ import { Provider as Provider3, ProviderScope as ProviderScope3 } from "@frontmc
959
1006
  import {
960
1007
  createMemoryStorage as createMemoryStorage2,
961
1008
  createNamespacedStorage,
962
- createStorage as createStorage2
1009
+ createStorage as createStorage2,
1010
+ StorageNotSupportedError
963
1011
  } from "@frontmcp/utils";
964
1012
  function escapePattern(str) {
965
1013
  return str.replace(/[*?[\]\\]/g, "\\$&");
966
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
+ }
967
1018
  var REVOCATION_RETENTION_SECONDS = 24 * 60 * 60;
968
1019
  var TIME_LIMITED_KEY_SEGMENT = `scope:${"time_limited" /* TIME_LIMITED */}`;
969
1020
  var ApprovalStorageStore = class {
@@ -1201,6 +1252,31 @@ var ApprovalStorageStore = class {
1201
1252
  await this.storage.mdelete(keysToDelete);
1202
1253
  return true;
1203
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
+ }
1204
1280
  async getRevocations(toolId, sessionId, userId, context) {
1205
1281
  this.ensureInitialized();
1206
1282
  const records = [];
@@ -1367,7 +1443,7 @@ __publicField(ApprovalPlugin, "dynamicProviders", (options) => {
1367
1443
  name: "approval:service",
1368
1444
  provide: ApprovalServiceToken,
1369
1445
  scope: ProviderScope4.CONTEXT,
1370
- inject: () => [ApprovalStoreToken, FRONTMCP_CONTEXT, ScopeEntry],
1446
+ inject: () => [ApprovalStoreToken, FRONTMCP_CONTEXT, ScopeEntry2],
1371
1447
  useFactory: (store, ctx, scope) => {
1372
1448
  const { sessionId, userId } = resolveApprovalIdentity(ctx);
1373
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.7",
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.7",
53
- "@frontmcp/utils": "1.8.7",
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.7",
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.
@@ -1007,6 +1051,9 @@ var import_utils3 = require("@frontmcp/utils");
1007
1051
  function escapePattern(str) {
1008
1052
  return str.replace(/[*?[\]\\]/g, "\\$&");
1009
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
+ }
1010
1057
  var REVOCATION_RETENTION_SECONDS = 24 * 60 * 60;
1011
1058
  var TIME_LIMITED_KEY_SEGMENT = `scope:${"time_limited" /* TIME_LIMITED */}`;
1012
1059
  var ApprovalStorageStore = class {
@@ -1244,6 +1291,31 @@ var ApprovalStorageStore = class {
1244
1291
  await this.storage.mdelete(keysToDelete);
1245
1292
  return true;
1246
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
+ }
1247
1319
  async getRevocations(toolId, sessionId, userId, context) {
1248
1320
  this.ensureInitialized();
1249
1321
  const records = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/plugin-approval",
3
- "version": "1.8.7",
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.7",
53
- "@frontmcp/utils": "1.8.7",
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.7",
58
+ "@frontmcp/lazy-zod": "1.9.0",
59
59
  "@vercel/kv": "^2.0.0 || ^3.0.0"
60
60
  },
61
61
  "peerDependenciesMeta": {
@@ -88,6 +88,14 @@ export declare class ApprovalStorageStore implements ApprovalStore {
88
88
  * (session, user, time-limited and context approvals alike). Recorded denials are kept.
89
89
  */
90
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;
91
99
  getRevocations(toolId: string, sessionId: string, userId?: string, context?: ApprovalContext): Promise<ApprovalRecord[]>;
92
100
  isApproved(toolId: string, sessionId: string, userId?: string, context?: ApprovalContext): Promise<boolean>;
93
101
  clearSessionApprovals(sessionId: string): Promise<number>;
@@ -1 +1 @@
1
- {"version":3,"file":"approval-storage.store.d.ts","sourceRoot":"","sources":["../../src/stores/approval-storage.store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"approval-storage.store.d.ts","sourceRoot":"","sources":["../../src/stores/approval-storage.store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAKL,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;AAsCpC;;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,gGAAgG;IAChG,OAAO,CAAC,iBAAiB,CAAC,CAAoB;IAC9C,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,KAAK,WAAW,GAMtB;IAED,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;IA+CtE;;;;OAIG;IACG,eAAe,CACnB,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,OAAO,CAAC;IAanB,mGAAmG;YACrF,iBAAiB;IASzB,cAAc,CAClB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,EAAE,CAAC;IAWtB,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;CAa7B;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,IAAI,CAAC,2BAA2B,EAAE,SAAS,GAAG,iBAAiB,CAAM,GAC7E,oBAAoB,CAMtB"}
@@ -101,6 +101,15 @@ 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>;
104
113
  /**
105
114
  * The caller's recent revocations of a tool: each is the approval that was revoked, with `revokedBy`,
106
115
  * `revokedAt` and `revocationReason`. Optional: a store that keeps no revocations leaves it out.
@@ -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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"}