@frontmcp/plugin-approval 1.7.2 → 1.8.1

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/index.js CHANGED
@@ -76,11 +76,37 @@ __export(index_exports, {
76
76
  module.exports = __toCommonJS(index_exports);
77
77
 
78
78
  // plugins/plugin-approval/src/approval.plugin.ts
79
- var import_sdk5 = require("@frontmcp/sdk");
79
+ var import_sdk6 = require("@frontmcp/sdk");
80
80
 
81
- // plugins/plugin-approval/src/stores/approval-storage.store.ts
81
+ // plugins/plugin-approval/src/approval.identity.ts
82
82
  var import_sdk = require("@frontmcp/sdk");
83
83
  var import_utils = require("@frontmcp/utils");
84
+ function nonEmptyString(value) {
85
+ return typeof value === "string" && value.length > 0 ? value : void 0;
86
+ }
87
+ function resolveApprovalIdentity(ctx) {
88
+ const extra = ctx?.authInfo?.extra;
89
+ const userId = nonEmptyString(extra?.["userId"]) ?? nonEmptyString(extra?.["sub"]) ?? nonEmptyString(ctx?.authInfo?.clientId);
90
+ const sessionId = nonEmptyString(ctx?.sessionId);
91
+ if (sessionId && sessionId !== import_sdk.STATELESS_SESSION_ID) {
92
+ return { sessionId, userId };
93
+ }
94
+ return { sessionId: userId ? `stateless-user:${userId}` : `unidentified:${(0, import_utils.randomUUID)()}`, userId };
95
+ }
96
+
97
+ // plugins/plugin-approval/src/approval.symbols.ts
98
+ var ApprovalStoreToken = /* @__PURE__ */ Symbol.for(
99
+ "plugin:approval:store"
100
+ );
101
+ var ApprovalServiceToken = /* @__PURE__ */ Symbol.for(
102
+ "plugin:approval:service"
103
+ );
104
+ var ChallengeServiceToken = /* @__PURE__ */ Symbol.for(
105
+ "plugin:approval:challenge-service"
106
+ );
107
+
108
+ // plugins/plugin-approval/src/hooks/approval-check.hook.ts
109
+ var import_sdk2 = require("@frontmcp/sdk");
84
110
 
85
111
  // plugins/plugin-approval/src/approval/types.ts
86
112
  var ApprovalScope = /* @__PURE__ */ ((ApprovalScope2) => {
@@ -445,269 +471,125 @@ var ChallengeValidationError = class extends ApprovalError {
445
471
  }
446
472
  };
447
473
 
448
- // plugins/plugin-approval/src/stores/approval-storage.store.ts
449
- function escapePattern(str) {
450
- return str.replace(/[*?[\]\\]/g, "\\$&");
451
- }
452
- var ApprovalStorageStore = class {
453
- storage;
454
- options;
455
- cleanupInterval;
456
- initialized = false;
457
- ownedStorage = false;
458
- constructor(options = {}) {
459
- this.options = {
460
- storage: options.storage ?? { type: "auto" },
461
- storageInstance: options.storageInstance,
462
- namespace: options.namespace ?? "approval",
463
- cleanupIntervalSeconds: options.cleanupIntervalSeconds ?? 60
464
- };
465
- }
466
- /**
467
- * Initialize the storage connection.
468
- */
469
- async initialize() {
470
- if (this.initialized) {
474
+ // plugins/plugin-approval/src/hooks/approval-check.hook.ts
475
+ var passedApprovalStores = /* @__PURE__ */ new WeakMap();
476
+ var ApprovalCheckPlugin = class extends import_sdk2.DynamicPlugin {
477
+ async checkApproval(flowCtx) {
478
+ const { tool, toolContext } = flowCtx.state;
479
+ if (!tool || !toolContext) return;
480
+ const metadata = tool.metadata;
481
+ const approvalConfig = this.resolveApprovalConfig(
482
+ metadata["approval"]
483
+ );
484
+ if (!approvalConfig.required) {
471
485
  return;
472
486
  }
473
- if (this.options.storageInstance) {
474
- this.storage = this.options.storageInstance.namespace(this.options.namespace);
475
- this.ownedStorage = false;
476
- } else {
477
- const rootStorage = await (0, import_utils.createStorage)(this.options.storage);
478
- this.storage = rootStorage.namespace(this.options.namespace);
479
- this.ownedStorage = true;
480
- }
481
- if (this.options.cleanupIntervalSeconds > 0) {
482
- this.cleanupInterval = setInterval(() => {
483
- void this.clearExpiredApprovals();
484
- }, this.options.cleanupIntervalSeconds * 1e3);
485
- this.cleanupInterval.unref?.();
487
+ if (approvalConfig.skipApproval) {
488
+ return;
486
489
  }
487
- this.initialized = true;
490
+ const approvalStore = this.get(ApprovalStoreToken);
491
+ const passedStores = passedApprovalStores.get(toolContext) ?? /* @__PURE__ */ new WeakSet();
492
+ if (passedStores.has(approvalStore)) return;
493
+ await this.enforceApproval(flowCtx, tool, toolContext, approvalConfig, approvalStore);
494
+ passedStores.add(approvalStore);
495
+ passedApprovalStores.set(toolContext, passedStores);
488
496
  }
489
- ensureInitialized() {
490
- if (!this.initialized) {
491
- throw new Error("ApprovalStorageStore not initialized. Call initialize() first.");
497
+ async enforceApproval(flowCtx, tool, toolContext, approvalConfig, approvalStore) {
498
+ const { sessionId, userId } = resolveApprovalIdentity(toolContext.tryGetContext?.());
499
+ const approval = await approvalStore.getApproval(tool.fullName, sessionId, userId);
500
+ if (approval?.state === "denied" /* DENIED */) {
501
+ throw new ApprovalRequiredError({
502
+ toolId: tool.fullName,
503
+ state: "denied",
504
+ message: `Tool "${tool.fullName}" execution denied.`
505
+ });
492
506
  }
493
- }
494
- buildKey(toolId, sessionId, userId, context) {
495
- const parts = [toolId];
496
- if (sessionId) parts.push(`session:${sessionId}`);
497
- if (userId) parts.push(`user:${userId}`);
498
- if (context) parts.push(`ctx:${context.type}:${context.identifier}`);
499
- return parts.join(":");
500
- }
501
- parseRecord(value) {
502
- if (!value) return void 0;
503
- try {
504
- const parsed = JSON.parse(value);
505
- const result = approvalRecordSchema.safeParse(parsed);
506
- if (!result.success) {
507
- return void 0;
508
- }
509
- return result.data;
510
- } catch {
511
- return void 0;
507
+ if (this.isPreApprovedContext(approvalConfig, this.getCurrentContext(flowCtx))) {
508
+ return;
512
509
  }
513
- }
514
- isExpired(approval) {
515
- return approval.expiresAt !== void 0 && Date.now() > approval.expiresAt;
516
- }
517
- async getApproval(toolId, sessionId, userId) {
518
- this.ensureInitialized();
519
- const sessionKey = this.buildKey(toolId, sessionId);
520
- const sessionValue = await this.storage.get(sessionKey);
521
- const sessionApproval = this.parseRecord(sessionValue);
522
- if (sessionApproval && !this.isExpired(sessionApproval)) {
523
- return sessionApproval;
510
+ if (approvalConfig.alwaysPrompt) {
511
+ await this.handleApprovalRequired(flowCtx, approvalConfig, approval);
512
+ return;
524
513
  }
525
- if (userId) {
526
- const userKey = this.buildKey(toolId, void 0, userId);
527
- const userValue = await this.storage.get(userKey);
528
- const userApproval = this.parseRecord(userValue);
529
- if (userApproval && !this.isExpired(userApproval)) {
530
- return userApproval;
514
+ if (approval?.state === "approved" /* APPROVED */) {
515
+ if (!this.isExpired(approval)) {
516
+ return;
531
517
  }
532
518
  }
533
- return void 0;
519
+ await this.handleApprovalRequired(flowCtx, approvalConfig, approval);
534
520
  }
535
- async queryApprovals(query) {
536
- this.ensureInitialized();
537
- const results = [];
538
- const pattern = query.toolId ? `${query.toolId}:*` : "*";
539
- const keys = await this.storage.keys(pattern);
540
- const values = await this.storage.mget(keys);
541
- for (const value of values) {
542
- const approval = this.parseRecord(value);
543
- if (!approval) continue;
544
- if (!query.includeExpired && this.isExpired(approval)) {
545
- continue;
546
- }
547
- if (query.toolId && approval.toolId !== query.toolId) continue;
548
- if (query.toolIds && !query.toolIds.includes(approval.toolId)) continue;
549
- if (query.scope && approval.scope !== query.scope) continue;
550
- if (query.scopes && !query.scopes.includes(approval.scope)) continue;
551
- if (query.state && approval.state !== query.state) continue;
552
- if (query.states && !query.states.includes(approval.state)) continue;
553
- if (query.sessionId && approval.sessionId !== query.sessionId) continue;
554
- if (query.userId && approval.userId !== query.userId) continue;
555
- if (query.context) {
556
- if (!approval.context || approval.context.type !== query.context.type || approval.context.identifier !== query.context.identifier) {
557
- continue;
558
- }
559
- }
560
- results.push(approval);
521
+ resolveApprovalConfig(config) {
522
+ if (config === true) {
523
+ return { required: true, defaultScope: "session" /* SESSION */ };
561
524
  }
562
- return results;
563
- }
564
- async grantApproval(options) {
565
- this.ensureInitialized();
566
- const now = Date.now();
567
- const expiresAt = options.ttlMs ? now + options.ttlMs : void 0;
568
- const grantedBy = normalizeGrantor(options.grantedBy);
569
- const record = {
570
- toolId: options.toolId,
571
- state: "approved" /* APPROVED */,
572
- scope: options.scope,
573
- grantedAt: now,
574
- expiresAt,
575
- ttlMs: options.ttlMs,
576
- sessionId: options.sessionId,
577
- userId: options.userId,
578
- context: options.context,
579
- grantedBy,
580
- reason: options.reason,
581
- metadata: options.metadata
525
+ if (config === false || config === void 0) {
526
+ return { required: false };
527
+ }
528
+ return {
529
+ ...config,
530
+ required: config.required ?? true,
531
+ defaultScope: config.defaultScope ?? "session" /* SESSION */
582
532
  };
583
- const key = this.buildKey(options.toolId, options.sessionId, options.userId, options.context);
584
- const ttlSeconds = options.ttlMs ? Math.ceil(options.ttlMs / 1e3) : void 0;
585
- await this.storage.set(key, JSON.stringify(record), { ttlSeconds });
586
- return record;
587
533
  }
588
- async revokeApproval(options) {
589
- this.ensureInitialized();
590
- const key = this.buildKey(options.toolId, options.sessionId, options.userId, options.context);
591
- const exists = await this.storage.exists(key);
592
- if (exists) {
593
- await this.storage.delete(key);
594
- return true;
595
- }
596
- return false;
534
+ isExpired(approval) {
535
+ if (!approval.expiresAt) return false;
536
+ return Date.now() > approval.expiresAt;
597
537
  }
598
- async isApproved(toolId, sessionId, userId, context) {
599
- this.ensureInitialized();
600
- if (context) {
601
- const contextKey = this.buildKey(toolId, sessionId, userId, context);
602
- const contextValue = await this.storage.get(contextKey);
603
- const contextApproval = this.parseRecord(contextValue);
604
- if (contextApproval && contextApproval.state === "approved" /* APPROVED */ && !this.isExpired(contextApproval)) {
605
- return true;
606
- }
607
- }
608
- const sessionKey = this.buildKey(toolId, sessionId);
609
- const sessionValue = await this.storage.get(sessionKey);
610
- const sessionApproval = this.parseRecord(sessionValue);
611
- if (sessionApproval && sessionApproval.state === "approved" /* APPROVED */ && !this.isExpired(sessionApproval)) {
612
- return true;
613
- }
614
- if (userId) {
615
- const userKey = this.buildKey(toolId, void 0, userId);
616
- const userValue = await this.storage.get(userKey);
617
- const userApproval = this.parseRecord(userValue);
618
- if (userApproval && userApproval.state === "approved" /* APPROVED */ && !this.isExpired(userApproval)) {
619
- return true;
620
- }
621
- }
622
- return false;
538
+ /**
539
+ * The context this call is running in, as established by the SESSION (GHSA-r848-p7wf-96rc).
540
+ *
541
+ * Deliberately ignores `toolContext.input.context`. The only consumer of this value is
542
+ * `isPreApprovedContext`, which skips the approval prompt outright, so reading it from the
543
+ * arguments of the call being gated let a caller name its own pre-approved context and walk
544
+ * straight past the gate. Only `authInfo.extra.approvalContext`, which the server sets
545
+ * while authenticating, can say what context a call belongs to.
546
+ */
547
+ getCurrentContext(flowCtx) {
548
+ const ctx = flowCtx.state.toolContext?.tryGetContext?.();
549
+ const sessionContext = ctx?.authInfo?.extra?.["approvalContext"];
550
+ return this.isApprovalContext(sessionContext) ? sessionContext : void 0;
623
551
  }
624
- async clearSessionApprovals(sessionId) {
625
- this.ensureInitialized();
626
- const escapedSessionId = escapePattern(sessionId);
627
- const pattern = `*:session:${escapedSessionId}*`;
628
- const keys = await this.storage.keys(pattern);
629
- if (keys.length === 0) {
630
- return 0;
631
- }
632
- return await this.storage.mdelete(keys);
552
+ isApprovalContext(value) {
553
+ return typeof value === "object" && value !== null && "type" in value && "identifier" in value && typeof value.type === "string" && typeof value.identifier === "string";
633
554
  }
634
- async clearExpiredApprovals() {
635
- this.ensureInitialized();
636
- const now = Date.now();
637
- const keys = await this.storage.keys("*");
638
- const values = await this.storage.mget(keys);
639
- const keysToDelete = [];
640
- for (let i = 0; i < keys.length; i++) {
641
- const approval = this.parseRecord(values[i]);
642
- if (approval && approval.expiresAt && approval.expiresAt <= now) {
643
- keysToDelete.push(keys[i]);
644
- }
645
- }
646
- if (keysToDelete.length > 0) {
647
- return await this.storage.mdelete(keysToDelete);
555
+ isPreApprovedContext(config, currentContext) {
556
+ if (!currentContext || !config.preApprovedContexts?.length) {
557
+ return false;
648
558
  }
649
- return 0;
559
+ return config.preApprovedContexts.some(
560
+ (preApproved) => preApproved.type === currentContext.type && preApproved.identifier === currentContext.identifier
561
+ );
650
562
  }
651
- async getStats() {
652
- this.ensureInitialized();
653
- const byScope = {
654
- ["session" /* SESSION */]: 0,
655
- ["user" /* USER */]: 0,
656
- ["time_limited" /* TIME_LIMITED */]: 0,
657
- ["tool_specific" /* TOOL_SPECIFIC */]: 0,
658
- ["context_specific" /* CONTEXT_SPECIFIC */]: 0
659
- };
660
- const byState = {
661
- ["pending" /* PENDING */]: 0,
662
- ["approved" /* APPROVED */]: 0,
663
- ["denied" /* DENIED */]: 0,
664
- ["expired" /* EXPIRED */]: 0
665
- };
666
- const keys = await this.storage.keys("*");
667
- const values = await this.storage.mget(keys);
668
- let total = 0;
669
- for (const value of values) {
670
- const approval = this.parseRecord(value);
671
- if (approval) {
672
- total++;
673
- byScope[approval.scope]++;
674
- byState[approval.state]++;
563
+ async handleApprovalRequired(flowCtx, config, existingApproval) {
564
+ const { tool } = flowCtx.state;
565
+ const message = config.approvalMessage ?? `Tool "${tool?.fullName}" requires approval to execute. Allow?`;
566
+ const isExpiredApproval = existingApproval ? this.isExpired(existingApproval) : false;
567
+ throw new ApprovalRequiredError({
568
+ toolId: tool?.fullName ?? "unknown",
569
+ state: isExpiredApproval ? "expired" : "pending",
570
+ message,
571
+ approvalOptions: {
572
+ allowedScopes: config.allowedScopes,
573
+ defaultScope: config.defaultScope,
574
+ maxTtlMs: config.maxTtlMs,
575
+ category: config.category,
576
+ riskLevel: config.riskLevel
675
577
  }
676
- }
677
- return {
678
- totalApprovals: total,
679
- byScope,
680
- byState
681
- };
682
- }
683
- async close() {
684
- if (this.cleanupInterval) {
685
- clearInterval(this.cleanupInterval);
686
- this.cleanupInterval = void 0;
687
- }
688
- if (this.ownedStorage && this.storage) {
689
- await this.storage.root.disconnect();
690
- }
691
- this.initialized = false;
578
+ });
692
579
  }
693
580
  };
694
- ApprovalStorageStore = __decorateClass([
695
- (0, import_sdk.Provider)({
696
- name: "provider:approval:store:storage",
697
- description: "Storage-backed approval store (supports Memory, Redis, Vercel KV, Upstash)",
698
- scope: import_sdk.ProviderScope.GLOBAL
581
+ __decorateClass([
582
+ import_sdk2.ToolHook.Will("execute", { priority: 100 })
583
+ ], ApprovalCheckPlugin.prototype, "checkApproval", 1);
584
+ ApprovalCheckPlugin = __decorateClass([
585
+ (0, import_sdk2.Plugin)({
586
+ name: "approval:check",
587
+ description: "Checks tool approval state before execution"
699
588
  })
700
- ], ApprovalStorageStore);
701
- function createApprovalMemoryStore(options = {}) {
702
- const memoryStorage = (0, import_utils.createMemoryStorage)();
703
- return new ApprovalStorageStore({
704
- ...options,
705
- storageInstance: memoryStorage
706
- });
707
- }
589
+ ], ApprovalCheckPlugin);
708
590
 
709
591
  // plugins/plugin-approval/src/services/approval.service.ts
710
- var import_sdk2 = require("@frontmcp/sdk");
592
+ var import_sdk3 = require("@frontmcp/sdk");
711
593
  var ApprovalService = class {
712
594
  constructor(store, sessionId, userId) {
713
595
  this.store = store;
@@ -849,10 +731,10 @@ var ApprovalService = class {
849
731
  }
850
732
  };
851
733
  ApprovalService = __decorateClass([
852
- (0, import_sdk2.Provider)({
734
+ (0, import_sdk3.Provider)({
853
735
  name: "provider:approval:service",
854
736
  description: "Service for managing tool approvals",
855
- scope: import_sdk2.ProviderScope.CONTEXT
737
+ scope: import_sdk3.ProviderScope.CONTEXT
856
738
  })
857
739
  ], ApprovalService);
858
740
  function createApprovalService(store, sessionId, userId) {
@@ -860,7 +742,7 @@ function createApprovalService(store, sessionId, userId) {
860
742
  }
861
743
 
862
744
  // plugins/plugin-approval/src/services/challenge.service.ts
863
- var import_sdk3 = require("@frontmcp/sdk");
745
+ var import_sdk4 = require("@frontmcp/sdk");
864
746
  var import_utils2 = require("@frontmcp/utils");
865
747
  var ChallengeService = class {
866
748
  storage;
@@ -994,10 +876,10 @@ var ChallengeService = class {
994
876
  }
995
877
  };
996
878
  ChallengeService = __decorateClass([
997
- (0, import_sdk3.Provider)({
879
+ (0, import_sdk4.Provider)({
998
880
  name: "provider:approval:challenge-service",
999
881
  description: "PKCE challenge service for webhook approval flows",
1000
- scope: import_sdk3.ProviderScope.GLOBAL
882
+ scope: import_sdk4.ProviderScope.GLOBAL
1001
883
  })
1002
884
  ], ChallengeService);
1003
885
  function generatePkcePairFromVerifier(codeVerifier) {
@@ -1014,132 +896,272 @@ function createMemoryChallengeService(options = {}) {
1014
896
  });
1015
897
  }
1016
898
 
1017
- // plugins/plugin-approval/src/approval.symbols.ts
1018
- var ApprovalStoreToken = /* @__PURE__ */ Symbol.for(
1019
- "plugin:approval:store"
1020
- );
1021
- var ApprovalServiceToken = /* @__PURE__ */ Symbol.for(
1022
- "plugin:approval:service"
1023
- );
1024
- var ChallengeServiceToken = /* @__PURE__ */ Symbol.for(
1025
- "plugin:approval:challenge-service"
1026
- );
1027
-
1028
- // plugins/plugin-approval/src/hooks/approval-check.hook.ts
1029
- var import_sdk4 = require("@frontmcp/sdk");
1030
- var ApprovalCheckPlugin = class extends import_sdk4.DynamicPlugin {
1031
- async checkApproval(flowCtx) {
1032
- const { tool, toolContext } = flowCtx.state;
1033
- if (!tool || !toolContext) return;
1034
- const metadata = tool.metadata;
1035
- const approvalConfig = this.resolveApprovalConfig(
1036
- metadata["approval"]
1037
- );
1038
- if (!approvalConfig.required) {
899
+ // plugins/plugin-approval/src/stores/approval-storage.store.ts
900
+ var import_sdk5 = require("@frontmcp/sdk");
901
+ var import_utils3 = require("@frontmcp/utils");
902
+ function escapePattern(str) {
903
+ return str.replace(/[*?[\]\\]/g, "\\$&");
904
+ }
905
+ var ApprovalStorageStore = class {
906
+ storage;
907
+ options;
908
+ cleanupInterval;
909
+ initialized = false;
910
+ ownedStorage = false;
911
+ constructor(options = {}) {
912
+ this.options = {
913
+ storage: options.storage ?? { type: "auto" },
914
+ storageInstance: options.storageInstance,
915
+ namespace: options.namespace ?? "approval",
916
+ cleanupIntervalSeconds: options.cleanupIntervalSeconds ?? 60
917
+ };
918
+ }
919
+ /**
920
+ * Initialize the storage connection.
921
+ */
922
+ async initialize() {
923
+ if (this.initialized) {
1039
924
  return;
1040
925
  }
1041
- if (approvalConfig.skipApproval) {
1042
- return;
926
+ if (this.options.storageInstance) {
927
+ this.storage = this.options.storageInstance.namespace(this.options.namespace);
928
+ this.ownedStorage = false;
929
+ } else {
930
+ const rootStorage = await (0, import_utils3.createStorage)(this.options.storage);
931
+ this.storage = rootStorage.namespace(this.options.namespace);
932
+ this.ownedStorage = true;
1043
933
  }
1044
- const ctx = toolContext.tryGetContext?.();
1045
- const sessionId = ctx?.sessionId ?? "unknown";
1046
- const userId = this.getStringExtra(ctx?.authInfo?.extra, "userId") ?? this.getStringExtra(ctx?.authInfo?.extra, "sub") ?? ctx?.authInfo?.clientId;
1047
- const currentContext = this.getCurrentContext(flowCtx);
1048
- if (this.isPreApprovedContext(approvalConfig, currentContext)) {
1049
- return;
934
+ if (this.options.cleanupIntervalSeconds > 0) {
935
+ this.cleanupInterval = setInterval(() => {
936
+ void this.clearExpiredApprovals();
937
+ }, this.options.cleanupIntervalSeconds * 1e3);
938
+ this.cleanupInterval.unref?.();
1050
939
  }
1051
- const approvalStore = this.get(ApprovalStoreToken);
1052
- const approval = await approvalStore.getApproval(tool.fullName, sessionId, userId);
1053
- if (approvalConfig.alwaysPrompt) {
1054
- await this.handleApprovalRequired(flowCtx, approvalConfig, approval);
1055
- return;
940
+ this.initialized = true;
941
+ }
942
+ ensureInitialized() {
943
+ if (!this.initialized) {
944
+ throw new Error("ApprovalStorageStore not initialized. Call initialize() first.");
1056
945
  }
1057
- if (approval?.state === "approved" /* APPROVED */) {
1058
- if (!this.isExpired(approval)) {
1059
- return;
946
+ }
947
+ buildKey(toolId, sessionId, userId, context) {
948
+ const parts = [toolId];
949
+ if (sessionId) parts.push(`session:${sessionId}`);
950
+ if (userId) parts.push(`user:${userId}`);
951
+ if (context) parts.push(`ctx:${context.type}:${context.identifier}`);
952
+ return parts.join(":");
953
+ }
954
+ parseRecord(value) {
955
+ if (!value) return void 0;
956
+ try {
957
+ const parsed = JSON.parse(value);
958
+ const result = approvalRecordSchema.safeParse(parsed);
959
+ if (!result.success) {
960
+ return void 0;
1060
961
  }
962
+ return result.data;
963
+ } catch {
964
+ return void 0;
1061
965
  }
1062
- if (approval?.state === "denied" /* DENIED */) {
1063
- throw new ApprovalRequiredError({
1064
- toolId: tool.fullName,
1065
- state: "denied",
1066
- message: `Tool "${tool.fullName}" execution denied.`
1067
- });
1068
- }
1069
- await this.handleApprovalRequired(flowCtx, approvalConfig, approval);
1070
966
  }
1071
- resolveApprovalConfig(config) {
1072
- if (config === true) {
1073
- return { required: true, defaultScope: "session" /* SESSION */ };
967
+ isExpired(approval) {
968
+ return approval.expiresAt !== void 0 && Date.now() > approval.expiresAt;
969
+ }
970
+ /**
971
+ * The session record, else the user record, except that a denial in either scope wins,
972
+ * so a session approval cannot mask a user-level denial.
973
+ */
974
+ async getApproval(toolId, sessionId, userId) {
975
+ this.ensureInitialized();
976
+ const keys = [this.buildKey(toolId, sessionId)];
977
+ if (userId) {
978
+ keys.push(this.buildKey(toolId, void 0, userId));
1074
979
  }
1075
- if (config === false || config === void 0) {
1076
- return { required: false };
980
+ const activeRecords = [];
981
+ for (const key of keys) {
982
+ const record = this.parseRecord(await this.storage.get(key));
983
+ if (record && !this.isExpired(record)) {
984
+ activeRecords.push(record);
985
+ }
1077
986
  }
1078
- return {
1079
- ...config,
1080
- required: config.required ?? true,
1081
- defaultScope: config.defaultScope ?? "session" /* SESSION */
1082
- };
987
+ return activeRecords.find((record) => record.state === "denied" /* DENIED */) ?? activeRecords[0];
1083
988
  }
1084
- isExpired(approval) {
1085
- if (!approval.expiresAt) return false;
1086
- return Date.now() > approval.expiresAt;
989
+ async queryApprovals(query) {
990
+ this.ensureInitialized();
991
+ const results = [];
992
+ const pattern = query.toolId ? `${query.toolId}:*` : "*";
993
+ const keys = await this.storage.keys(pattern);
994
+ const values = await this.storage.mget(keys);
995
+ for (const value of values) {
996
+ const approval = this.parseRecord(value);
997
+ if (!approval) continue;
998
+ if (!query.includeExpired && this.isExpired(approval)) {
999
+ continue;
1000
+ }
1001
+ if (query.toolId && approval.toolId !== query.toolId) continue;
1002
+ if (query.toolIds && !query.toolIds.includes(approval.toolId)) continue;
1003
+ if (query.scope && approval.scope !== query.scope) continue;
1004
+ if (query.scopes && !query.scopes.includes(approval.scope)) continue;
1005
+ if (query.state && approval.state !== query.state) continue;
1006
+ if (query.states && !query.states.includes(approval.state)) continue;
1007
+ if (query.sessionId && approval.sessionId !== query.sessionId) continue;
1008
+ if (query.userId && approval.userId !== query.userId) continue;
1009
+ if (query.context) {
1010
+ if (!approval.context || approval.context.type !== query.context.type || approval.context.identifier !== query.context.identifier) {
1011
+ continue;
1012
+ }
1013
+ }
1014
+ results.push(approval);
1015
+ }
1016
+ return results;
1087
1017
  }
1088
- getCurrentContext(flowCtx) {
1089
- const { toolContext } = flowCtx.state;
1090
- const ctx = toolContext?.tryGetContext?.();
1091
- const inputContext = toolContext?.input?.["context"];
1092
- const contextFromInput = this.isApprovalContext(inputContext) ? inputContext : void 0;
1093
- const sessionContext = ctx?.authInfo?.extra?.["approvalContext"];
1094
- const contextFromSession = this.isApprovalContext(sessionContext) ? sessionContext : void 0;
1095
- return contextFromInput ?? contextFromSession;
1018
+ async grantApproval(options) {
1019
+ this.ensureInitialized();
1020
+ const now = Date.now();
1021
+ const expiresAt = options.ttlMs ? now + options.ttlMs : void 0;
1022
+ const grantedBy = normalizeGrantor(options.grantedBy);
1023
+ const record = {
1024
+ toolId: options.toolId,
1025
+ state: "approved" /* APPROVED */,
1026
+ scope: options.scope,
1027
+ grantedAt: now,
1028
+ expiresAt,
1029
+ ttlMs: options.ttlMs,
1030
+ sessionId: options.sessionId,
1031
+ userId: options.userId,
1032
+ context: options.context,
1033
+ grantedBy,
1034
+ reason: options.reason,
1035
+ metadata: options.metadata
1036
+ };
1037
+ const key = this.buildKey(options.toolId, options.sessionId, options.userId, options.context);
1038
+ const ttlSeconds = options.ttlMs ? Math.ceil(options.ttlMs / 1e3) : void 0;
1039
+ await this.storage.set(key, JSON.stringify(record), { ttlSeconds });
1040
+ return record;
1096
1041
  }
1097
- isApprovalContext(value) {
1098
- return typeof value === "object" && value !== null && "type" in value && "identifier" in value && typeof value.type === "string" && typeof value.identifier === "string";
1042
+ async revokeApproval(options) {
1043
+ this.ensureInitialized();
1044
+ const key = this.buildKey(options.toolId, options.sessionId, options.userId, options.context);
1045
+ const exists = await this.storage.exists(key);
1046
+ if (exists) {
1047
+ await this.storage.delete(key);
1048
+ return true;
1049
+ }
1050
+ return false;
1099
1051
  }
1100
- getStringExtra(extra, key) {
1101
- if (!extra) return void 0;
1102
- const value = extra[key];
1103
- return typeof value === "string" ? value : void 0;
1052
+ async isApproved(toolId, sessionId, userId, context) {
1053
+ this.ensureInitialized();
1054
+ if (context) {
1055
+ const contextKey = this.buildKey(toolId, sessionId, userId, context);
1056
+ const contextValue = await this.storage.get(contextKey);
1057
+ const contextApproval = this.parseRecord(contextValue);
1058
+ if (contextApproval && contextApproval.state === "approved" /* APPROVED */ && !this.isExpired(contextApproval)) {
1059
+ return true;
1060
+ }
1061
+ }
1062
+ const sessionKey = this.buildKey(toolId, sessionId);
1063
+ const sessionValue = await this.storage.get(sessionKey);
1064
+ const sessionApproval = this.parseRecord(sessionValue);
1065
+ if (sessionApproval && sessionApproval.state === "approved" /* APPROVED */ && !this.isExpired(sessionApproval)) {
1066
+ return true;
1067
+ }
1068
+ if (userId) {
1069
+ const userKey = this.buildKey(toolId, void 0, userId);
1070
+ const userValue = await this.storage.get(userKey);
1071
+ const userApproval = this.parseRecord(userValue);
1072
+ if (userApproval && userApproval.state === "approved" /* APPROVED */ && !this.isExpired(userApproval)) {
1073
+ return true;
1074
+ }
1075
+ }
1076
+ return false;
1104
1077
  }
1105
- isPreApprovedContext(config, currentContext) {
1106
- if (!currentContext || !config.preApprovedContexts?.length) {
1107
- return false;
1078
+ async clearSessionApprovals(sessionId) {
1079
+ this.ensureInitialized();
1080
+ const escapedSessionId = escapePattern(sessionId);
1081
+ const pattern = `*:session:${escapedSessionId}*`;
1082
+ const keys = await this.storage.keys(pattern);
1083
+ if (keys.length === 0) {
1084
+ return 0;
1108
1085
  }
1109
- return config.preApprovedContexts.some(
1110
- (preApproved) => preApproved.type === currentContext.type && preApproved.identifier === currentContext.identifier
1111
- );
1086
+ return await this.storage.mdelete(keys);
1112
1087
  }
1113
- async handleApprovalRequired(flowCtx, config, existingApproval) {
1114
- const { tool } = flowCtx.state;
1115
- const message = config.approvalMessage ?? `Tool "${tool?.fullName}" requires approval to execute. Allow?`;
1116
- const isExpiredApproval = existingApproval ? this.isExpired(existingApproval) : false;
1117
- throw new ApprovalRequiredError({
1118
- toolId: tool?.fullName ?? "unknown",
1119
- state: isExpiredApproval ? "expired" : "pending",
1120
- message,
1121
- approvalOptions: {
1122
- allowedScopes: config.allowedScopes,
1123
- defaultScope: config.defaultScope,
1124
- maxTtlMs: config.maxTtlMs,
1125
- category: config.category,
1126
- riskLevel: config.riskLevel
1088
+ async clearExpiredApprovals() {
1089
+ this.ensureInitialized();
1090
+ const now = Date.now();
1091
+ const keys = await this.storage.keys("*");
1092
+ const values = await this.storage.mget(keys);
1093
+ const keysToDelete = [];
1094
+ for (let i = 0; i < keys.length; i++) {
1095
+ const approval = this.parseRecord(values[i]);
1096
+ if (approval && approval.expiresAt && approval.expiresAt <= now) {
1097
+ keysToDelete.push(keys[i]);
1127
1098
  }
1128
- });
1099
+ }
1100
+ if (keysToDelete.length > 0) {
1101
+ return await this.storage.mdelete(keysToDelete);
1102
+ }
1103
+ return 0;
1104
+ }
1105
+ async getStats() {
1106
+ this.ensureInitialized();
1107
+ const byScope = {
1108
+ ["session" /* SESSION */]: 0,
1109
+ ["user" /* USER */]: 0,
1110
+ ["time_limited" /* TIME_LIMITED */]: 0,
1111
+ ["tool_specific" /* TOOL_SPECIFIC */]: 0,
1112
+ ["context_specific" /* CONTEXT_SPECIFIC */]: 0
1113
+ };
1114
+ const byState = {
1115
+ ["pending" /* PENDING */]: 0,
1116
+ ["approved" /* APPROVED */]: 0,
1117
+ ["denied" /* DENIED */]: 0,
1118
+ ["expired" /* EXPIRED */]: 0
1119
+ };
1120
+ const keys = await this.storage.keys("*");
1121
+ const values = await this.storage.mget(keys);
1122
+ let total = 0;
1123
+ for (const value of values) {
1124
+ const approval = this.parseRecord(value);
1125
+ if (approval) {
1126
+ total++;
1127
+ byScope[approval.scope]++;
1128
+ byState[approval.state]++;
1129
+ }
1130
+ }
1131
+ return {
1132
+ totalApprovals: total,
1133
+ byScope,
1134
+ byState
1135
+ };
1136
+ }
1137
+ async close() {
1138
+ if (this.cleanupInterval) {
1139
+ clearInterval(this.cleanupInterval);
1140
+ this.cleanupInterval = void 0;
1141
+ }
1142
+ if (this.ownedStorage && this.storage) {
1143
+ await this.storage.root.disconnect();
1144
+ }
1145
+ this.initialized = false;
1129
1146
  }
1130
1147
  };
1131
- __decorateClass([
1132
- import_sdk4.ToolHook.Will("execute", { priority: 100 })
1133
- ], ApprovalCheckPlugin.prototype, "checkApproval", 1);
1134
- ApprovalCheckPlugin = __decorateClass([
1135
- (0, import_sdk4.Plugin)({
1136
- name: "approval:check",
1137
- description: "Checks tool approval state before execution"
1148
+ ApprovalStorageStore = __decorateClass([
1149
+ (0, import_sdk5.Provider)({
1150
+ name: "provider:approval:store:storage",
1151
+ description: "Storage-backed approval store (supports Memory, Redis, Vercel KV, Upstash)",
1152
+ scope: import_sdk5.ProviderScope.GLOBAL
1138
1153
  })
1139
- ], ApprovalCheckPlugin);
1154
+ ], ApprovalStorageStore);
1155
+ function createApprovalMemoryStore(options = {}) {
1156
+ const memoryStorage = (0, import_utils3.createMemoryStorage)();
1157
+ return new ApprovalStorageStore({
1158
+ ...options,
1159
+ storageInstance: memoryStorage
1160
+ });
1161
+ }
1140
1162
 
1141
1163
  // plugins/plugin-approval/src/approval.plugin.ts
1142
- var ApprovalPlugin = class extends import_sdk5.DynamicPlugin {
1164
+ var ApprovalPlugin = class extends import_sdk6.DynamicPlugin {
1143
1165
  options;
1144
1166
  constructor(options = {}) {
1145
1167
  super();
@@ -1148,12 +1170,6 @@ var ApprovalPlugin = class extends import_sdk5.DynamicPlugin {
1148
1170
  ...options
1149
1171
  };
1150
1172
  }
1151
- /**
1152
- * Get plugin metadata including nested plugins.
1153
- */
1154
- static getPluginMetadata(_options) {
1155
- return { plugins: [ApprovalCheckPlugin] };
1156
- }
1157
1173
  };
1158
1174
  __publicField(ApprovalPlugin, "defaultOptions", {
1159
1175
  namespace: "approval",
@@ -1206,19 +1222,20 @@ __publicField(ApprovalPlugin, "dynamicProviders", (options) => {
1206
1222
  providers.push({
1207
1223
  name: "approval:service",
1208
1224
  provide: ApprovalServiceToken,
1209
- scope: import_sdk5.ProviderScope.CONTEXT,
1210
- inject: () => [ApprovalStoreToken, import_sdk5.FRONTMCP_CONTEXT],
1225
+ scope: import_sdk6.ProviderScope.CONTEXT,
1226
+ inject: () => [ApprovalStoreToken, import_sdk6.FRONTMCP_CONTEXT],
1211
1227
  useFactory: (store, ctx) => {
1212
- const userId = ctx.authInfo?.extra?.["userId"] ?? ctx.authInfo?.extra?.["sub"] ?? ctx.authInfo?.clientId;
1213
- return createApprovalService(store, ctx.sessionId, userId);
1228
+ const { sessionId, userId } = resolveApprovalIdentity(ctx);
1229
+ return createApprovalService(store, sessionId, userId);
1214
1230
  }
1215
1231
  });
1216
1232
  return providers;
1217
1233
  });
1218
1234
  ApprovalPlugin = __decorateClass([
1219
- (0, import_sdk5.Plugin)({
1235
+ (0, import_sdk6.Plugin)({
1220
1236
  name: "approval",
1221
1237
  description: "Tool authorization workflow with PKCE webhook security",
1238
+ plugins: [ApprovalCheckPlugin],
1222
1239
  contextExtensions: [
1223
1240
  {
1224
1241
  property: "approval",