@frontmcp/plugin-approval 1.8.5 → 1.8.7
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/README.md +18 -0
- package/approval/errors.d.ts +6 -5
- package/approval/errors.d.ts.map +1 -1
- package/esm/index.mjs +75 -16
- package/esm/package.json +4 -4
- package/index.js +91 -33
- package/package.json +4 -4
- package/services/approval.service.d.ts +20 -2
- package/services/approval.service.d.ts.map +1 -1
- package/stores/approval-storage.store.d.ts +4 -0
- package/stores/approval-storage.store.d.ts.map +1 -1
- package/stores/approval-store.interface.d.ts +6 -1
- package/stores/approval-store.interface.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,24 @@ class DangerousTool extends ToolContext {
|
|
|
72
72
|
}
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
A refused call reaches the client as an error result whose text is exactly the tool's `approvalMessage` and whose
|
|
76
|
+
`_meta.code` is `APPROVAL_REQUIRED`, in production too: the approval errors extend `PublicMcpError`.
|
|
77
|
+
|
|
78
|
+
A grant made through `this.approval` records the signed-in caller as its grantor, `userGrantor(<user id>)`
|
|
79
|
+
(`{ source: 'user', identifier, method: 'interactive' }`), or `{ source: 'user' }` when there is no signed-in user
|
|
80
|
+
(an anonymous `anon:` subject included). Pass `grantedBy` to record something else:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { policyGrantor, userGrantor } from '@frontmcp/plugin-approval';
|
|
84
|
+
|
|
85
|
+
await this.approval.grantSessionApproval('ops:deploy', { grantedBy: policyGrantor('safe-list') });
|
|
86
|
+
await this.approval.grantUserApproval('ops:deploy', {
|
|
87
|
+
grantedBy: userGrantor('user-123', 'Jane Doe', { method: 'interactive' }),
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`revokeApproval()` records `revokedBy` by the same rule.
|
|
92
|
+
|
|
75
93
|
### Approval Scopes
|
|
76
94
|
|
|
77
95
|
| Scope | Description |
|
package/approval/errors.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Error classes for approval operations.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* They are public MCP errors: a refusal is an answer for the caller, so its message reaches the
|
|
5
|
+
* client as written, in every environment, instead of being wrapped as an internal server error.
|
|
6
6
|
*
|
|
7
|
-
* @module @frontmcp/
|
|
7
|
+
* @module @frontmcp/plugin-approval
|
|
8
8
|
*/
|
|
9
|
+
import { PublicMcpError } from '@frontmcp/sdk';
|
|
9
10
|
import type { ApprovalScope } from './types';
|
|
10
11
|
/**
|
|
11
12
|
* Base class for approval-related errors.
|
|
12
13
|
*/
|
|
13
|
-
export declare class ApprovalError extends
|
|
14
|
-
constructor(message: string);
|
|
14
|
+
export declare class ApprovalError extends PublicMcpError {
|
|
15
|
+
constructor(message: string, code?: string, statusCode?: number);
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* Error thrown when tool approval is required but not granted.
|
package/approval/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/approval/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;GAEG;AACH,qBAAa,aAAc,SAAQ,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/approval/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;GAEG;AACH,qBAAa,aAAc,SAAQ,cAAc;gBACnC,OAAO,EAAE,MAAM,EAAE,IAAI,SAAsB,EAAE,UAAU,SAAM;CAI1E;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,aAAa;aAEpC,OAAO,EAAE;QACvB,sBAAsB;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,6BAA6B;QAC7B,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;QACxC,0BAA0B;QAC1B,OAAO,EAAE,MAAM,CAAC;QAChB,0CAA0C;QAC1C,eAAe,CAAC,EAAE;YAChB,aAAa,CAAC,EAAE,aAAa,EAAE,CAAC;YAChC,YAAY,CAAC,EAAE,aAAa,CAAC;YAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,CAAC;KACH;gBAfe,OAAO,EAAE;QACvB,sBAAsB;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,6BAA6B;QAC7B,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;QACxC,0BAA0B;QAC1B,OAAO,EAAE,MAAM,CAAC;QAChB,0CAA0C;QAC1C,eAAe,CAAC,EAAE;YAChB,aAAa,CAAC,EAAE,aAAa,EAAE,CAAC;YAChC,YAAY,CAAC,EAAE,aAAa,CAAC;YAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,CAAC;KACH;IAMH;;OAEG;IACH,cAAc;;;;;;;;gCAfQ,aAAa,EAAE;+BAChB,aAAa;2BACjB,MAAM;2BACN,MAAM;4BACL,MAAM;;;;CAuBzB;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,aAAa;aAErC,SAAS,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO;aACvC,MAAM,EAAE,MAAM;gBADd,SAAS,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,EACvC,MAAM,EAAE,MAAM;IAMhC;;OAEG;IACH,cAAc;;;;;;;;CAUf;AAED;;GAEG;AACH,qBAAa,4BAA6B,SAAQ,aAAa;aAE3C,cAAc,EAAE,aAAa;aAC7B,aAAa,EAAE,aAAa,EAAE;gBAD9B,cAAc,EAAE,aAAa,EAC7B,aAAa,EAAE,aAAa,EAAE;IAWhD;;OAEG;IACH,cAAc;;;;;;;;;CAWf;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,aAAa;aAEnC,MAAM,EAAE,MAAM;aACd,SAAS,EAAE,MAAM;gBADjB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM;IAMnC;;OAEG;IACH,cAAc;;;;;;;;;CAWf;AAED;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,aAAa;aAEvC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,cAAc;gBAA5D,MAAM,GAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,cAA0B,EACxF,OAAO,CAAC,EAAE,MAAM;IAMlB;;OAEG;IACH,cAAc;;;;;;;;CAUf"}
|
package/esm/index.mjs
CHANGED
|
@@ -27,9 +27,10 @@ import {
|
|
|
27
27
|
} from "@frontmcp/sdk";
|
|
28
28
|
|
|
29
29
|
// plugins/plugin-approval/src/approval/errors.ts
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
import { PublicMcpError } from "@frontmcp/sdk";
|
|
31
|
+
var ApprovalError = class extends PublicMcpError {
|
|
32
|
+
constructor(message, code = "APPROVAL_REQUIRED", statusCode = 403) {
|
|
33
|
+
super(message, code, statusCode);
|
|
33
34
|
this.name = "ApprovalError";
|
|
34
35
|
}
|
|
35
36
|
};
|
|
@@ -59,7 +60,7 @@ var ApprovalRequiredError = class extends ApprovalError {
|
|
|
59
60
|
};
|
|
60
61
|
var ApprovalOperationError = class extends ApprovalError {
|
|
61
62
|
constructor(operation, reason) {
|
|
62
|
-
super(`Approval ${operation} failed: ${reason}
|
|
63
|
+
super(`Approval ${operation} failed: ${reason}`, "APPROVAL_OPERATION_FAILED", 400);
|
|
63
64
|
this.operation = operation;
|
|
64
65
|
this.reason = reason;
|
|
65
66
|
this.name = "ApprovalOperationError";
|
|
@@ -84,7 +85,9 @@ var ApprovalOperationError = class extends ApprovalError {
|
|
|
84
85
|
var ApprovalScopeNotAllowedError = class extends ApprovalError {
|
|
85
86
|
constructor(requestedScope, allowedScopes) {
|
|
86
87
|
super(
|
|
87
|
-
`Approval scope '${requestedScope}' is not allowed for this tool. Allowed scopes: ${allowedScopes.join(", ")}
|
|
88
|
+
`Approval scope '${requestedScope}' is not allowed for this tool. Allowed scopes: ${allowedScopes.join(", ")}`,
|
|
89
|
+
"APPROVAL_SCOPE_NOT_ALLOWED",
|
|
90
|
+
400
|
|
88
91
|
);
|
|
89
92
|
this.requestedScope = requestedScope;
|
|
90
93
|
this.allowedScopes = allowedScopes;
|
|
@@ -110,7 +113,7 @@ var ApprovalScopeNotAllowedError = class extends ApprovalError {
|
|
|
110
113
|
};
|
|
111
114
|
var ApprovalExpiredError = class extends ApprovalError {
|
|
112
115
|
constructor(toolId, expiredAt) {
|
|
113
|
-
super(`Approval for tool '${toolId}' expired at ${new Date(expiredAt).toISOString()}
|
|
116
|
+
super(`Approval for tool '${toolId}' expired at ${new Date(expiredAt).toISOString()}`, "APPROVAL_EXPIRED", 403);
|
|
114
117
|
this.toolId = toolId;
|
|
115
118
|
this.expiredAt = expiredAt;
|
|
116
119
|
this.name = "ApprovalExpiredError";
|
|
@@ -135,7 +138,7 @@ var ApprovalExpiredError = class extends ApprovalError {
|
|
|
135
138
|
};
|
|
136
139
|
var ChallengeValidationError = class extends ApprovalError {
|
|
137
140
|
constructor(reason = "invalid", message) {
|
|
138
|
-
super(message ?? `PKCE challenge validation failed: ${reason}
|
|
141
|
+
super(message ?? `PKCE challenge validation failed: ${reason}`, "CHALLENGE_VALIDATION_FAILED", 400);
|
|
139
142
|
this.reason = reason;
|
|
140
143
|
this.name = "ChallengeValidationError";
|
|
141
144
|
}
|
|
@@ -589,7 +592,7 @@ ApprovalCheckPlugin = __decorateClass([
|
|
|
589
592
|
], ApprovalCheckPlugin);
|
|
590
593
|
|
|
591
594
|
// plugins/plugin-approval/src/services/approval.service.ts
|
|
592
|
-
import { Provider, ProviderScope } from "@frontmcp/sdk";
|
|
595
|
+
import { isAnonymousSubject, Provider, ProviderScope } from "@frontmcp/sdk";
|
|
593
596
|
var ApprovalService = class {
|
|
594
597
|
constructor(store, sessionId, userId, requirementOf = () => void 0) {
|
|
595
598
|
this.store = store;
|
|
@@ -605,6 +608,27 @@ var ApprovalService = class {
|
|
|
605
608
|
checkGrant(toolId, scope, ttlMs) {
|
|
606
609
|
return checkGrantAgainstPolicy(toolId, scope, ttlMs, this.requirementOf(toolId));
|
|
607
610
|
}
|
|
611
|
+
/** The caller's user id, when it names a principal: an anonymous subject (`anon:…`) names no one. */
|
|
612
|
+
get signedInUserId() {
|
|
613
|
+
return this.userId && !isAnonymousSubject(this.userId) ? this.userId : void 0;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Who a grant is recorded as when the call names no one: the signed-in user whose tool call made it.
|
|
617
|
+
* 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 a user with no identifier.
|
|
619
|
+
*
|
|
620
|
+
* The method is `'implicit'`: a tool that grants through `this.approval` asked no one, so the grant
|
|
621
|
+
* is not an `'interactive'` answer. A tool that did ask passes `grantedBy: userGrantor(id)`.
|
|
622
|
+
*/
|
|
623
|
+
defaultGrantor() {
|
|
624
|
+
const userId = this.signedInUserId;
|
|
625
|
+
return userId ? userGrantor(userId, void 0, { method: "implicit" }) : { source: "user", method: "implicit" };
|
|
626
|
+
}
|
|
627
|
+
/** Who a revocation is recorded as when the call names no one, by the same rule as a grant. */
|
|
628
|
+
defaultRevoker() {
|
|
629
|
+
const userId = this.signedInUserId;
|
|
630
|
+
return userId ? { source: "user", identifier: userId, method: "implicit" } : { source: "user", method: "implicit" };
|
|
631
|
+
}
|
|
608
632
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
609
633
|
// Query Methods
|
|
610
634
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -660,6 +684,13 @@ var ApprovalService = class {
|
|
|
660
684
|
(record) => record.sessionId === this.sessionId || this.userId !== void 0 && record.userId === this.userId
|
|
661
685
|
);
|
|
662
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* The caller's recent revocations of a tool, newest last: each is the approval that was revoked,
|
|
689
|
+
* with `revokedBy`, `revokedAt` and `revocationReason`. Empty when the store keeps no revocations.
|
|
690
|
+
*/
|
|
691
|
+
async getRevocations(toolId) {
|
|
692
|
+
return await this.store.getRevocations?.(toolId, this.sessionId, this.userId) ?? [];
|
|
693
|
+
}
|
|
663
694
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
664
695
|
// Grant Methods
|
|
665
696
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -673,7 +704,7 @@ var ApprovalService = class {
|
|
|
673
704
|
scope: "session" /* SESSION */,
|
|
674
705
|
ttlMs,
|
|
675
706
|
sessionId: this.sessionId,
|
|
676
|
-
grantedBy: options.grantedBy ??
|
|
707
|
+
grantedBy: options.grantedBy ?? this.defaultGrantor(),
|
|
677
708
|
reason: options.reason,
|
|
678
709
|
metadata: options.metadata
|
|
679
710
|
});
|
|
@@ -691,7 +722,7 @@ var ApprovalService = class {
|
|
|
691
722
|
scope: "user" /* USER */,
|
|
692
723
|
ttlMs,
|
|
693
724
|
userId: this.userId,
|
|
694
|
-
grantedBy: options.grantedBy ??
|
|
725
|
+
grantedBy: options.grantedBy ?? this.defaultGrantor(),
|
|
695
726
|
reason: options.reason,
|
|
696
727
|
metadata: options.metadata
|
|
697
728
|
});
|
|
@@ -707,7 +738,7 @@ var ApprovalService = class {
|
|
|
707
738
|
ttlMs,
|
|
708
739
|
sessionId: this.sessionId,
|
|
709
740
|
userId: this.userId,
|
|
710
|
-
grantedBy: options.grantedBy ??
|
|
741
|
+
grantedBy: options.grantedBy ?? this.defaultGrantor(),
|
|
711
742
|
reason: options.reason,
|
|
712
743
|
metadata: options.metadata
|
|
713
744
|
});
|
|
@@ -724,7 +755,7 @@ var ApprovalService = class {
|
|
|
724
755
|
context,
|
|
725
756
|
sessionId: this.sessionId,
|
|
726
757
|
userId: this.userId,
|
|
727
|
-
grantedBy: options.grantedBy ??
|
|
758
|
+
grantedBy: options.grantedBy ?? this.defaultGrantor(),
|
|
728
759
|
reason: options.reason,
|
|
729
760
|
metadata: options.metadata
|
|
730
761
|
});
|
|
@@ -741,7 +772,7 @@ var ApprovalService = class {
|
|
|
741
772
|
toolId,
|
|
742
773
|
sessionId: this.sessionId,
|
|
743
774
|
userId: this.userId,
|
|
744
|
-
revokedBy: options.revokedBy ??
|
|
775
|
+
revokedBy: options.revokedBy ?? this.defaultRevoker(),
|
|
745
776
|
reason: options.reason
|
|
746
777
|
});
|
|
747
778
|
}
|
|
@@ -927,14 +958,18 @@ function createMemoryChallengeService(options = {}) {
|
|
|
927
958
|
import { Provider as Provider3, ProviderScope as ProviderScope3 } from "@frontmcp/sdk";
|
|
928
959
|
import {
|
|
929
960
|
createMemoryStorage as createMemoryStorage2,
|
|
961
|
+
createNamespacedStorage,
|
|
930
962
|
createStorage as createStorage2
|
|
931
963
|
} from "@frontmcp/utils";
|
|
932
964
|
function escapePattern(str) {
|
|
933
965
|
return str.replace(/[*?[\]\\]/g, "\\$&");
|
|
934
966
|
}
|
|
967
|
+
var REVOCATION_RETENTION_SECONDS = 24 * 60 * 60;
|
|
935
968
|
var TIME_LIMITED_KEY_SEGMENT = `scope:${"time_limited" /* TIME_LIMITED */}`;
|
|
936
969
|
var ApprovalStorageStore = class {
|
|
937
970
|
storage;
|
|
971
|
+
/** Revoked approvals live beside the live ones, in a namespace no live-approval query scans. */
|
|
972
|
+
revocationStorage;
|
|
938
973
|
options;
|
|
939
974
|
cleanupInterval;
|
|
940
975
|
initialized = false;
|
|
@@ -970,6 +1005,13 @@ var ApprovalStorageStore = class {
|
|
|
970
1005
|
}
|
|
971
1006
|
this.initialized = true;
|
|
972
1007
|
}
|
|
1008
|
+
get revocations() {
|
|
1009
|
+
this.revocationStorage ??= createNamespacedStorage(
|
|
1010
|
+
this.storage.root,
|
|
1011
|
+
`${this.storage.prefix.replace(/:$/, "")}-revoked`
|
|
1012
|
+
);
|
|
1013
|
+
return this.revocationStorage;
|
|
1014
|
+
}
|
|
973
1015
|
ensureInitialized() {
|
|
974
1016
|
if (!this.initialized) {
|
|
975
1017
|
throw new Error("ApprovalStorageStore not initialized. Call initialize() first.");
|
|
@@ -1131,27 +1173,43 @@ var ApprovalStorageStore = class {
|
|
|
1131
1173
|
const keys = await this.storage.keys(`${escapePattern(toolId)}:*`);
|
|
1132
1174
|
const values = await this.storage.mget(keys);
|
|
1133
1175
|
const keysToDelete = [];
|
|
1176
|
+
const revoked = /* @__PURE__ */ new Map();
|
|
1134
1177
|
for (let i = 0; i < keys.length; i++) {
|
|
1135
1178
|
const record = this.parseRecord(values[i]);
|
|
1136
1179
|
if (!record || record.toolId !== toolId || record.state === "denied" /* DENIED */) continue;
|
|
1180
|
+
const matches = keysToDelete.length;
|
|
1137
1181
|
if (context) {
|
|
1138
1182
|
const sameSession = sessionId !== void 0 && record.sessionId === sessionId;
|
|
1139
1183
|
if (record.context?.type === context.type && record.context.identifier === context.identifier && (!sessionId || sameSession) && // A context approval granted to the session before it had a user applies to that user too.
|
|
1140
1184
|
(!userId || record.userId === userId || sameSession && record.userId === void 0)) {
|
|
1141
1185
|
keysToDelete.push(keys[i]);
|
|
1142
1186
|
}
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1145
|
-
if (sessionId && record.sessionId === sessionId || userId && record.userId === userId) {
|
|
1187
|
+
} else if (sessionId && record.sessionId === sessionId || userId && record.userId === userId) {
|
|
1146
1188
|
keysToDelete.push(keys[i]);
|
|
1147
1189
|
}
|
|
1190
|
+
if (keysToDelete.length > matches) revoked.set(keys[i], record);
|
|
1148
1191
|
}
|
|
1149
1192
|
if (keysToDelete.length === 0) {
|
|
1150
1193
|
return false;
|
|
1151
1194
|
}
|
|
1195
|
+
const revokedAt = Date.now();
|
|
1196
|
+
const revokedBy = normalizeRevoker(options.revokedBy);
|
|
1197
|
+
for (const [key, record] of revoked) {
|
|
1198
|
+
const entry = { ...record, revokedAt, revokedBy, revocationReason: options.reason };
|
|
1199
|
+
await this.revocations.set(key, JSON.stringify(entry), { ttlSeconds: REVOCATION_RETENTION_SECONDS });
|
|
1200
|
+
}
|
|
1152
1201
|
await this.storage.mdelete(keysToDelete);
|
|
1153
1202
|
return true;
|
|
1154
1203
|
}
|
|
1204
|
+
async getRevocations(toolId, sessionId, userId, context) {
|
|
1205
|
+
this.ensureInitialized();
|
|
1206
|
+
const records = [];
|
|
1207
|
+
for (const key of this.callerKeys(toolId, sessionId, userId, context)) {
|
|
1208
|
+
const record = this.parseRecord(await this.revocations.get(key));
|
|
1209
|
+
if (record && record.toolId === toolId) records.push(record);
|
|
1210
|
+
}
|
|
1211
|
+
return records.sort((a, b) => (a.revokedAt ?? 0) - (b.revokedAt ?? 0));
|
|
1212
|
+
}
|
|
1155
1213
|
async isApproved(toolId, sessionId, userId, context) {
|
|
1156
1214
|
const approval = await this.getApproval(toolId, sessionId, userId, context);
|
|
1157
1215
|
return approval?.state === "approved" /* APPROVED */;
|
|
@@ -1227,6 +1285,7 @@ var ApprovalStorageStore = class {
|
|
|
1227
1285
|
if (this.ownedStorage && this.storage) {
|
|
1228
1286
|
await this.storage.root.disconnect();
|
|
1229
1287
|
}
|
|
1288
|
+
this.revocationStorage = void 0;
|
|
1230
1289
|
this.initialized = false;
|
|
1231
1290
|
}
|
|
1232
1291
|
};
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-approval",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.7",
|
|
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.
|
|
53
|
-
"@frontmcp/utils": "1.8.
|
|
52
|
+
"@frontmcp/sdk": "1.8.7",
|
|
53
|
+
"@frontmcp/utils": "1.8.7",
|
|
54
54
|
"ioredis": "^5.8.0",
|
|
55
55
|
"reflect-metadata": "^0.2.2"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@frontmcp/lazy-zod": "1.8.
|
|
58
|
+
"@frontmcp/lazy-zod": "1.8.7",
|
|
59
59
|
"@vercel/kv": "^2.0.0 || ^3.0.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
package/index.js
CHANGED
|
@@ -76,12 +76,13 @@ __export(index_exports, {
|
|
|
76
76
|
module.exports = __toCommonJS(index_exports);
|
|
77
77
|
|
|
78
78
|
// plugins/plugin-approval/src/approval.plugin.ts
|
|
79
|
-
var
|
|
79
|
+
var import_sdk6 = require("@frontmcp/sdk");
|
|
80
80
|
|
|
81
81
|
// plugins/plugin-approval/src/approval/errors.ts
|
|
82
|
-
var
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
var import_sdk = require("@frontmcp/sdk");
|
|
83
|
+
var ApprovalError = class extends import_sdk.PublicMcpError {
|
|
84
|
+
constructor(message, code = "APPROVAL_REQUIRED", statusCode = 403) {
|
|
85
|
+
super(message, code, statusCode);
|
|
85
86
|
this.name = "ApprovalError";
|
|
86
87
|
}
|
|
87
88
|
};
|
|
@@ -111,7 +112,7 @@ var ApprovalRequiredError = class extends ApprovalError {
|
|
|
111
112
|
};
|
|
112
113
|
var ApprovalOperationError = class extends ApprovalError {
|
|
113
114
|
constructor(operation, reason) {
|
|
114
|
-
super(`Approval ${operation} failed: ${reason}
|
|
115
|
+
super(`Approval ${operation} failed: ${reason}`, "APPROVAL_OPERATION_FAILED", 400);
|
|
115
116
|
this.operation = operation;
|
|
116
117
|
this.reason = reason;
|
|
117
118
|
this.name = "ApprovalOperationError";
|
|
@@ -136,7 +137,9 @@ var ApprovalOperationError = class extends ApprovalError {
|
|
|
136
137
|
var ApprovalScopeNotAllowedError = class extends ApprovalError {
|
|
137
138
|
constructor(requestedScope, allowedScopes) {
|
|
138
139
|
super(
|
|
139
|
-
`Approval scope '${requestedScope}' is not allowed for this tool. Allowed scopes: ${allowedScopes.join(", ")}
|
|
140
|
+
`Approval scope '${requestedScope}' is not allowed for this tool. Allowed scopes: ${allowedScopes.join(", ")}`,
|
|
141
|
+
"APPROVAL_SCOPE_NOT_ALLOWED",
|
|
142
|
+
400
|
|
140
143
|
);
|
|
141
144
|
this.requestedScope = requestedScope;
|
|
142
145
|
this.allowedScopes = allowedScopes;
|
|
@@ -162,7 +165,7 @@ var ApprovalScopeNotAllowedError = class extends ApprovalError {
|
|
|
162
165
|
};
|
|
163
166
|
var ApprovalExpiredError = class extends ApprovalError {
|
|
164
167
|
constructor(toolId, expiredAt) {
|
|
165
|
-
super(`Approval for tool '${toolId}' expired at ${new Date(expiredAt).toISOString()}
|
|
168
|
+
super(`Approval for tool '${toolId}' expired at ${new Date(expiredAt).toISOString()}`, "APPROVAL_EXPIRED", 403);
|
|
166
169
|
this.toolId = toolId;
|
|
167
170
|
this.expiredAt = expiredAt;
|
|
168
171
|
this.name = "ApprovalExpiredError";
|
|
@@ -187,7 +190,7 @@ var ApprovalExpiredError = class extends ApprovalError {
|
|
|
187
190
|
};
|
|
188
191
|
var ChallengeValidationError = class extends ApprovalError {
|
|
189
192
|
constructor(reason = "invalid", message) {
|
|
190
|
-
super(message ?? `PKCE challenge validation failed: ${reason}
|
|
193
|
+
super(message ?? `PKCE challenge validation failed: ${reason}`, "CHALLENGE_VALIDATION_FAILED", 400);
|
|
191
194
|
this.reason = reason;
|
|
192
195
|
this.name = "ChallengeValidationError";
|
|
193
196
|
}
|
|
@@ -304,7 +307,7 @@ var ChallengeServiceToken = /* @__PURE__ */ Symbol.for(
|
|
|
304
307
|
);
|
|
305
308
|
|
|
306
309
|
// plugins/plugin-approval/src/hooks/approval-check.hook.ts
|
|
307
|
-
var
|
|
310
|
+
var import_sdk2 = require("@frontmcp/sdk");
|
|
308
311
|
|
|
309
312
|
// plugins/plugin-approval/src/approval/schemas.ts
|
|
310
313
|
var import_lazy_zod = require("@frontmcp/lazy-zod");
|
|
@@ -524,7 +527,7 @@ function isApiGrantor(grantor) {
|
|
|
524
527
|
|
|
525
528
|
// plugins/plugin-approval/src/hooks/approval-check.hook.ts
|
|
526
529
|
var passedApprovalStores = /* @__PURE__ */ new WeakMap();
|
|
527
|
-
var ApprovalCheckPlugin = class extends
|
|
530
|
+
var ApprovalCheckPlugin = class extends import_sdk2.DynamicPlugin {
|
|
528
531
|
async checkApproval(flowCtx) {
|
|
529
532
|
const { tool, toolContext } = flowCtx.state;
|
|
530
533
|
if (!tool || !toolContext) return;
|
|
@@ -629,10 +632,10 @@ var ApprovalCheckPlugin = class extends import_sdk.DynamicPlugin {
|
|
|
629
632
|
}
|
|
630
633
|
};
|
|
631
634
|
__decorateClass([
|
|
632
|
-
|
|
635
|
+
import_sdk2.ToolHook.Will("execute", { priority: 100, appliesTo: "uncovered-apps" })
|
|
633
636
|
], ApprovalCheckPlugin.prototype, "checkApproval", 1);
|
|
634
637
|
ApprovalCheckPlugin = __decorateClass([
|
|
635
|
-
(0,
|
|
638
|
+
(0, import_sdk2.Plugin)({
|
|
636
639
|
name: "approval:check",
|
|
637
640
|
description: "Checks tool approval state before execution",
|
|
638
641
|
// A server where a tool or agent declares `approval` and this gate does not reach it refuses to start.
|
|
@@ -641,7 +644,7 @@ ApprovalCheckPlugin = __decorateClass([
|
|
|
641
644
|
], ApprovalCheckPlugin);
|
|
642
645
|
|
|
643
646
|
// plugins/plugin-approval/src/services/approval.service.ts
|
|
644
|
-
var
|
|
647
|
+
var import_sdk3 = require("@frontmcp/sdk");
|
|
645
648
|
var ApprovalService = class {
|
|
646
649
|
constructor(store, sessionId, userId, requirementOf = () => void 0) {
|
|
647
650
|
this.store = store;
|
|
@@ -657,6 +660,27 @@ var ApprovalService = class {
|
|
|
657
660
|
checkGrant(toolId, scope, ttlMs) {
|
|
658
661
|
return checkGrantAgainstPolicy(toolId, scope, ttlMs, this.requirementOf(toolId));
|
|
659
662
|
}
|
|
663
|
+
/** The caller's user id, when it names a principal: an anonymous subject (`anon:…`) names no one. */
|
|
664
|
+
get signedInUserId() {
|
|
665
|
+
return this.userId && !(0, import_sdk3.isAnonymousSubject)(this.userId) ? this.userId : void 0;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Who a grant is recorded as when the call names no one: the signed-in user whose tool call made it.
|
|
669
|
+
* 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 a user with no identifier.
|
|
671
|
+
*
|
|
672
|
+
* The method is `'implicit'`: a tool that grants through `this.approval` asked no one, so the grant
|
|
673
|
+
* is not an `'interactive'` answer. A tool that did ask passes `grantedBy: userGrantor(id)`.
|
|
674
|
+
*/
|
|
675
|
+
defaultGrantor() {
|
|
676
|
+
const userId = this.signedInUserId;
|
|
677
|
+
return userId ? userGrantor(userId, void 0, { method: "implicit" }) : { source: "user", method: "implicit" };
|
|
678
|
+
}
|
|
679
|
+
/** Who a revocation is recorded as when the call names no one, by the same rule as a grant. */
|
|
680
|
+
defaultRevoker() {
|
|
681
|
+
const userId = this.signedInUserId;
|
|
682
|
+
return userId ? { source: "user", identifier: userId, method: "implicit" } : { source: "user", method: "implicit" };
|
|
683
|
+
}
|
|
660
684
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
661
685
|
// Query Methods
|
|
662
686
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -712,6 +736,13 @@ var ApprovalService = class {
|
|
|
712
736
|
(record) => record.sessionId === this.sessionId || this.userId !== void 0 && record.userId === this.userId
|
|
713
737
|
);
|
|
714
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* The caller's recent revocations of a tool, newest last: each is the approval that was revoked,
|
|
741
|
+
* with `revokedBy`, `revokedAt` and `revocationReason`. Empty when the store keeps no revocations.
|
|
742
|
+
*/
|
|
743
|
+
async getRevocations(toolId) {
|
|
744
|
+
return await this.store.getRevocations?.(toolId, this.sessionId, this.userId) ?? [];
|
|
745
|
+
}
|
|
715
746
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
716
747
|
// Grant Methods
|
|
717
748
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -725,7 +756,7 @@ var ApprovalService = class {
|
|
|
725
756
|
scope: "session" /* SESSION */,
|
|
726
757
|
ttlMs,
|
|
727
758
|
sessionId: this.sessionId,
|
|
728
|
-
grantedBy: options.grantedBy ??
|
|
759
|
+
grantedBy: options.grantedBy ?? this.defaultGrantor(),
|
|
729
760
|
reason: options.reason,
|
|
730
761
|
metadata: options.metadata
|
|
731
762
|
});
|
|
@@ -743,7 +774,7 @@ var ApprovalService = class {
|
|
|
743
774
|
scope: "user" /* USER */,
|
|
744
775
|
ttlMs,
|
|
745
776
|
userId: this.userId,
|
|
746
|
-
grantedBy: options.grantedBy ??
|
|
777
|
+
grantedBy: options.grantedBy ?? this.defaultGrantor(),
|
|
747
778
|
reason: options.reason,
|
|
748
779
|
metadata: options.metadata
|
|
749
780
|
});
|
|
@@ -759,7 +790,7 @@ var ApprovalService = class {
|
|
|
759
790
|
ttlMs,
|
|
760
791
|
sessionId: this.sessionId,
|
|
761
792
|
userId: this.userId,
|
|
762
|
-
grantedBy: options.grantedBy ??
|
|
793
|
+
grantedBy: options.grantedBy ?? this.defaultGrantor(),
|
|
763
794
|
reason: options.reason,
|
|
764
795
|
metadata: options.metadata
|
|
765
796
|
});
|
|
@@ -776,7 +807,7 @@ var ApprovalService = class {
|
|
|
776
807
|
context,
|
|
777
808
|
sessionId: this.sessionId,
|
|
778
809
|
userId: this.userId,
|
|
779
|
-
grantedBy: options.grantedBy ??
|
|
810
|
+
grantedBy: options.grantedBy ?? this.defaultGrantor(),
|
|
780
811
|
reason: options.reason,
|
|
781
812
|
metadata: options.metadata
|
|
782
813
|
});
|
|
@@ -793,7 +824,7 @@ var ApprovalService = class {
|
|
|
793
824
|
toolId,
|
|
794
825
|
sessionId: this.sessionId,
|
|
795
826
|
userId: this.userId,
|
|
796
|
-
revokedBy: options.revokedBy ??
|
|
827
|
+
revokedBy: options.revokedBy ?? this.defaultRevoker(),
|
|
797
828
|
reason: options.reason
|
|
798
829
|
});
|
|
799
830
|
}
|
|
@@ -805,10 +836,10 @@ var ApprovalService = class {
|
|
|
805
836
|
}
|
|
806
837
|
};
|
|
807
838
|
ApprovalService = __decorateClass([
|
|
808
|
-
(0,
|
|
839
|
+
(0, import_sdk3.Provider)({
|
|
809
840
|
name: "provider:approval:service",
|
|
810
841
|
description: "Service for managing tool approvals",
|
|
811
|
-
scope:
|
|
842
|
+
scope: import_sdk3.ProviderScope.CONTEXT
|
|
812
843
|
})
|
|
813
844
|
], ApprovalService);
|
|
814
845
|
function createApprovalService(store, sessionId, userId, requirementOf) {
|
|
@@ -816,7 +847,7 @@ function createApprovalService(store, sessionId, userId, requirementOf) {
|
|
|
816
847
|
}
|
|
817
848
|
|
|
818
849
|
// plugins/plugin-approval/src/services/challenge.service.ts
|
|
819
|
-
var
|
|
850
|
+
var import_sdk4 = require("@frontmcp/sdk");
|
|
820
851
|
var import_utils2 = require("@frontmcp/utils");
|
|
821
852
|
var ChallengeService = class {
|
|
822
853
|
storage;
|
|
@@ -950,10 +981,10 @@ var ChallengeService = class {
|
|
|
950
981
|
}
|
|
951
982
|
};
|
|
952
983
|
ChallengeService = __decorateClass([
|
|
953
|
-
(0,
|
|
984
|
+
(0, import_sdk4.Provider)({
|
|
954
985
|
name: "provider:approval:challenge-service",
|
|
955
986
|
description: "PKCE challenge service for webhook approval flows",
|
|
956
|
-
scope:
|
|
987
|
+
scope: import_sdk4.ProviderScope.GLOBAL
|
|
957
988
|
})
|
|
958
989
|
], ChallengeService);
|
|
959
990
|
function generatePkcePairFromVerifier(codeVerifier) {
|
|
@@ -971,14 +1002,17 @@ function createMemoryChallengeService(options = {}) {
|
|
|
971
1002
|
}
|
|
972
1003
|
|
|
973
1004
|
// plugins/plugin-approval/src/stores/approval-storage.store.ts
|
|
974
|
-
var
|
|
1005
|
+
var import_sdk5 = require("@frontmcp/sdk");
|
|
975
1006
|
var import_utils3 = require("@frontmcp/utils");
|
|
976
1007
|
function escapePattern(str) {
|
|
977
1008
|
return str.replace(/[*?[\]\\]/g, "\\$&");
|
|
978
1009
|
}
|
|
1010
|
+
var REVOCATION_RETENTION_SECONDS = 24 * 60 * 60;
|
|
979
1011
|
var TIME_LIMITED_KEY_SEGMENT = `scope:${"time_limited" /* TIME_LIMITED */}`;
|
|
980
1012
|
var ApprovalStorageStore = class {
|
|
981
1013
|
storage;
|
|
1014
|
+
/** Revoked approvals live beside the live ones, in a namespace no live-approval query scans. */
|
|
1015
|
+
revocationStorage;
|
|
982
1016
|
options;
|
|
983
1017
|
cleanupInterval;
|
|
984
1018
|
initialized = false;
|
|
@@ -1014,6 +1048,13 @@ var ApprovalStorageStore = class {
|
|
|
1014
1048
|
}
|
|
1015
1049
|
this.initialized = true;
|
|
1016
1050
|
}
|
|
1051
|
+
get revocations() {
|
|
1052
|
+
this.revocationStorage ??= (0, import_utils3.createNamespacedStorage)(
|
|
1053
|
+
this.storage.root,
|
|
1054
|
+
`${this.storage.prefix.replace(/:$/, "")}-revoked`
|
|
1055
|
+
);
|
|
1056
|
+
return this.revocationStorage;
|
|
1057
|
+
}
|
|
1017
1058
|
ensureInitialized() {
|
|
1018
1059
|
if (!this.initialized) {
|
|
1019
1060
|
throw new Error("ApprovalStorageStore not initialized. Call initialize() first.");
|
|
@@ -1175,27 +1216,43 @@ var ApprovalStorageStore = class {
|
|
|
1175
1216
|
const keys = await this.storage.keys(`${escapePattern(toolId)}:*`);
|
|
1176
1217
|
const values = await this.storage.mget(keys);
|
|
1177
1218
|
const keysToDelete = [];
|
|
1219
|
+
const revoked = /* @__PURE__ */ new Map();
|
|
1178
1220
|
for (let i = 0; i < keys.length; i++) {
|
|
1179
1221
|
const record = this.parseRecord(values[i]);
|
|
1180
1222
|
if (!record || record.toolId !== toolId || record.state === "denied" /* DENIED */) continue;
|
|
1223
|
+
const matches = keysToDelete.length;
|
|
1181
1224
|
if (context) {
|
|
1182
1225
|
const sameSession = sessionId !== void 0 && record.sessionId === sessionId;
|
|
1183
1226
|
if (record.context?.type === context.type && record.context.identifier === context.identifier && (!sessionId || sameSession) && // A context approval granted to the session before it had a user applies to that user too.
|
|
1184
1227
|
(!userId || record.userId === userId || sameSession && record.userId === void 0)) {
|
|
1185
1228
|
keysToDelete.push(keys[i]);
|
|
1186
1229
|
}
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1189
|
-
if (sessionId && record.sessionId === sessionId || userId && record.userId === userId) {
|
|
1230
|
+
} else if (sessionId && record.sessionId === sessionId || userId && record.userId === userId) {
|
|
1190
1231
|
keysToDelete.push(keys[i]);
|
|
1191
1232
|
}
|
|
1233
|
+
if (keysToDelete.length > matches) revoked.set(keys[i], record);
|
|
1192
1234
|
}
|
|
1193
1235
|
if (keysToDelete.length === 0) {
|
|
1194
1236
|
return false;
|
|
1195
1237
|
}
|
|
1238
|
+
const revokedAt = Date.now();
|
|
1239
|
+
const revokedBy = normalizeRevoker(options.revokedBy);
|
|
1240
|
+
for (const [key, record] of revoked) {
|
|
1241
|
+
const entry = { ...record, revokedAt, revokedBy, revocationReason: options.reason };
|
|
1242
|
+
await this.revocations.set(key, JSON.stringify(entry), { ttlSeconds: REVOCATION_RETENTION_SECONDS });
|
|
1243
|
+
}
|
|
1196
1244
|
await this.storage.mdelete(keysToDelete);
|
|
1197
1245
|
return true;
|
|
1198
1246
|
}
|
|
1247
|
+
async getRevocations(toolId, sessionId, userId, context) {
|
|
1248
|
+
this.ensureInitialized();
|
|
1249
|
+
const records = [];
|
|
1250
|
+
for (const key of this.callerKeys(toolId, sessionId, userId, context)) {
|
|
1251
|
+
const record = this.parseRecord(await this.revocations.get(key));
|
|
1252
|
+
if (record && record.toolId === toolId) records.push(record);
|
|
1253
|
+
}
|
|
1254
|
+
return records.sort((a, b) => (a.revokedAt ?? 0) - (b.revokedAt ?? 0));
|
|
1255
|
+
}
|
|
1199
1256
|
async isApproved(toolId, sessionId, userId, context) {
|
|
1200
1257
|
const approval = await this.getApproval(toolId, sessionId, userId, context);
|
|
1201
1258
|
return approval?.state === "approved" /* APPROVED */;
|
|
@@ -1271,14 +1328,15 @@ var ApprovalStorageStore = class {
|
|
|
1271
1328
|
if (this.ownedStorage && this.storage) {
|
|
1272
1329
|
await this.storage.root.disconnect();
|
|
1273
1330
|
}
|
|
1331
|
+
this.revocationStorage = void 0;
|
|
1274
1332
|
this.initialized = false;
|
|
1275
1333
|
}
|
|
1276
1334
|
};
|
|
1277
1335
|
ApprovalStorageStore = __decorateClass([
|
|
1278
|
-
(0,
|
|
1336
|
+
(0, import_sdk5.Provider)({
|
|
1279
1337
|
name: "provider:approval:store:storage",
|
|
1280
1338
|
description: "Storage-backed approval store (supports Memory, Redis, Vercel KV, Upstash)",
|
|
1281
|
-
scope:
|
|
1339
|
+
scope: import_sdk5.ProviderScope.GLOBAL
|
|
1282
1340
|
})
|
|
1283
1341
|
], ApprovalStorageStore);
|
|
1284
1342
|
function createApprovalMemoryStore(options = {}) {
|
|
@@ -1290,7 +1348,7 @@ function createApprovalMemoryStore(options = {}) {
|
|
|
1290
1348
|
}
|
|
1291
1349
|
|
|
1292
1350
|
// plugins/plugin-approval/src/approval.plugin.ts
|
|
1293
|
-
var ApprovalPlugin = class extends
|
|
1351
|
+
var ApprovalPlugin = class extends import_sdk6.DynamicPlugin {
|
|
1294
1352
|
options;
|
|
1295
1353
|
constructor(options = {}) {
|
|
1296
1354
|
super();
|
|
@@ -1351,8 +1409,8 @@ __publicField(ApprovalPlugin, "dynamicProviders", (options) => {
|
|
|
1351
1409
|
providers.push({
|
|
1352
1410
|
name: "approval:service",
|
|
1353
1411
|
provide: ApprovalServiceToken,
|
|
1354
|
-
scope:
|
|
1355
|
-
inject: () => [ApprovalStoreToken,
|
|
1412
|
+
scope: import_sdk6.ProviderScope.CONTEXT,
|
|
1413
|
+
inject: () => [ApprovalStoreToken, import_sdk6.FRONTMCP_CONTEXT, import_sdk6.ScopeEntry],
|
|
1356
1414
|
useFactory: (store, ctx, scope) => {
|
|
1357
1415
|
const { sessionId, userId } = resolveApprovalIdentity(ctx);
|
|
1358
1416
|
const requirementOf = (toolId) => {
|
|
@@ -1367,7 +1425,7 @@ __publicField(ApprovalPlugin, "dynamicProviders", (options) => {
|
|
|
1367
1425
|
return providers;
|
|
1368
1426
|
});
|
|
1369
1427
|
ApprovalPlugin = __decorateClass([
|
|
1370
|
-
(0,
|
|
1428
|
+
(0, import_sdk6.Plugin)({
|
|
1371
1429
|
name: "approval",
|
|
1372
1430
|
description: "Tool authorization workflow with PKCE webhook security",
|
|
1373
1431
|
plugins: [ApprovalCheckPlugin],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-approval",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.7",
|
|
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.
|
|
53
|
-
"@frontmcp/utils": "1.8.
|
|
52
|
+
"@frontmcp/sdk": "1.8.7",
|
|
53
|
+
"@frontmcp/utils": "1.8.7",
|
|
54
54
|
"ioredis": "^5.8.0",
|
|
55
55
|
"reflect-metadata": "^0.2.2"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@frontmcp/lazy-zod": "1.8.
|
|
58
|
+
"@frontmcp/lazy-zod": "1.8.7",
|
|
59
59
|
"@vercel/kv": "^2.0.0 || ^3.0.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
@@ -14,7 +14,7 @@ export type ApprovalRequirementLookup = (toolId: string) => ToolApprovalRequirem
|
|
|
14
14
|
* Options for granting approvals via the service.
|
|
15
15
|
*/
|
|
16
16
|
export interface GrantOptions {
|
|
17
|
-
/** Who/what is granting the approval (defaults to '
|
|
17
|
+
/** Who/what is granting the approval (defaults to the signed-in user, or `{ source: 'user' }` without one) */
|
|
18
18
|
grantedBy?: ApprovalGrantor | ApprovalSourceType;
|
|
19
19
|
/** Optional reason for the approval */
|
|
20
20
|
reason?: string;
|
|
@@ -25,7 +25,7 @@ export interface GrantOptions {
|
|
|
25
25
|
* Options for revoking approvals via the service.
|
|
26
26
|
*/
|
|
27
27
|
export interface RevokeOptions {
|
|
28
|
-
/** Who/what is revoking the approval (defaults to '
|
|
28
|
+
/** Who/what is revoking the approval (defaults to the signed-in user, or `{ source: 'user' }` without one) */
|
|
29
29
|
revokedBy?: ApprovalRevoker | RevocationSourceType;
|
|
30
30
|
/** Optional reason for revocation */
|
|
31
31
|
reason?: string;
|
|
@@ -41,6 +41,19 @@ export declare class ApprovalService {
|
|
|
41
41
|
constructor(store: ApprovalStore, sessionId: string, userId?: string | undefined, requirementOf?: ApprovalRequirementLookup);
|
|
42
42
|
/** Checks a grant against the tool's `allowedScopes` and `maxTtlMs`; returns the ttl to store. */
|
|
43
43
|
private checkGrant;
|
|
44
|
+
/** The caller's user id, when it names a principal: an anonymous subject (`anon:…`) names no one. */
|
|
45
|
+
private get signedInUserId();
|
|
46
|
+
/**
|
|
47
|
+
* Who a grant is recorded as when the call names no one: the signed-in user whose tool call made it.
|
|
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 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)`.
|
|
53
|
+
*/
|
|
54
|
+
private defaultGrantor;
|
|
55
|
+
/** Who a revocation is recorded as when the call names no one, by the same rule as a grant. */
|
|
56
|
+
private defaultRevoker;
|
|
44
57
|
/**
|
|
45
58
|
* Check if a tool is approved for current session/user.
|
|
46
59
|
*/
|
|
@@ -63,6 +76,11 @@ export declare class ApprovalService {
|
|
|
63
76
|
* another caller's records. Read the store directly for administrative queries.
|
|
64
77
|
*/
|
|
65
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[]>;
|
|
66
84
|
/**
|
|
67
85
|
* Grant session-scoped approval for a tool.
|
|
68
86
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval.service.d.ts","sourceRoot":"","sources":["../../src/services/approval.service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
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,7 @@ 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
|
+
getRevocations(toolId: string, sessionId: string, userId?: string, context?: ApprovalContext): Promise<ApprovalRecord[]>;
|
|
88
92
|
isApproved(toolId: string, sessionId: string, userId?: string, context?: ApprovalContext): Promise<boolean>;
|
|
89
93
|
clearSessionApprovals(sessionId: string): Promise<number>;
|
|
90
94
|
clearExpiredApprovals(): Promise<number>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-storage.store.d.ts","sourceRoot":"","sources":["../../src/stores/approval-storage.store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,
|
|
1
|
+
{"version":3,"file":"approval-storage.store.d.ts","sourceRoot":"","sources":["../../src/stores/approval-storage.store.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module @frontmcp/plugin-approval
|
|
5
5
|
*/
|
|
6
|
-
import type {
|
|
6
|
+
import type { ApprovalContext, ApprovalGrantor, ApprovalRecord, ApprovalRevoker, ApprovalScope, ApprovalSourceType, ApprovalState, RevocationSourceType } from '../types';
|
|
7
7
|
/**
|
|
8
8
|
* Query options for finding approvals.
|
|
9
9
|
*/
|
|
@@ -101,6 +101,11 @@ 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
|
+
* The caller's recent revocations of a tool: each is the approval that was revoked, with `revokedBy`,
|
|
106
|
+
* `revokedAt` and `revocationReason`. Optional: a store that keeps no revocations leaves it out.
|
|
107
|
+
*/
|
|
108
|
+
getRevocations?(toolId: string, sessionId: string, userId?: string, context?: ApprovalContext): Promise<ApprovalRecord[]>;
|
|
104
109
|
/**
|
|
105
110
|
* Check if a tool is approved.
|
|
106
111
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-store.interface.d.ts","sourceRoot":"","sources":["../../src/stores/approval-store.interface.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"approval-store.interface.d.ts","sourceRoot":"","sources":["../../src/stores/approval-store.interface.ts"],"names":[],"mappings":"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"}
|