@forestadmin/workflow-executor 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/adapters/ai-client-adapter.d.ts +5 -1
- package/dist/adapters/ai-client-adapter.js +4 -1
- package/dist/adapters/always-error-ai-model-port.d.ts +5 -1
- package/dist/adapters/always-error-ai-model-port.js +6 -1
- package/dist/adapters/server-ai-adapter.d.ts +5 -1
- package/dist/adapters/server-ai-adapter.js +4 -1
- package/dist/adapters/step-outcome-to-update-step-mapper.js +4 -1
- package/dist/build-workflow-executor.js +87 -2
- package/dist/crypto/credential-encryption.d.ts +10 -0
- package/dist/crypto/credential-encryption.js +75 -0
- package/dist/defaults.d.ts +1 -0
- package/dist/defaults.js +5 -2
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +59 -2
- package/dist/executors/load-related-record-step-executor.d.ts +4 -9
- package/dist/executors/load-related-record-step-executor.js +164 -97
- package/dist/executors/mcp-step-executor.d.ts +7 -2
- package/dist/executors/mcp-step-executor.js +78 -14
- package/dist/executors/step-executor-factory.d.ts +1 -1
- package/dist/executors/step-executor-factory.js +24 -3
- package/dist/http/executor-http-server.d.ts +12 -0
- package/dist/http/executor-http-server.js +90 -1
- package/dist/http/mcp-oauth-credentials.d.ts +24 -0
- package/dist/http/mcp-oauth-credentials.js +77 -0
- package/dist/http/step-serializer.js +5 -2
- package/dist/index.d.ts +7 -1
- package/dist/index.js +10 -3
- package/dist/oauth/keyed-mutex.d.ts +5 -0
- package/dist/oauth/keyed-mutex.js +27 -0
- package/dist/oauth/refresh-grant.d.ts +15 -0
- package/dist/oauth/refresh-grant.js +112 -0
- package/dist/oauth/token-endpoint-url.d.ts +2 -0
- package/dist/oauth/token-endpoint-url.js +103 -0
- package/dist/oauth/token-service.d.ts +34 -0
- package/dist/oauth/token-service.js +172 -0
- package/dist/ports/ai-model-port.d.ts +5 -1
- package/dist/ports/mcp-oauth-credentials-store.d.ts +24 -0
- package/dist/ports/mcp-oauth-credentials-store.js +3 -0
- package/dist/ports/run-store.d.ts +1 -0
- package/dist/remote-tool-fetcher.d.ts +7 -2
- package/dist/remote-tool-fetcher.js +53 -6
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +3 -3
- package/dist/stores/database-mcp-oauth-credentials-store.d.ts +22 -0
- package/dist/stores/database-mcp-oauth-credentials-store.js +188 -0
- package/dist/stores/database-store.d.ts +1 -1
- package/dist/stores/database-store.js +17 -50
- package/dist/stores/in-memory-mcp-oauth-credentials-store.d.ts +13 -0
- package/dist/stores/in-memory-mcp-oauth-credentials-store.js +44 -0
- package/dist/stores/in-memory-store.d.ts +1 -0
- package/dist/stores/in-memory-store.js +6 -1
- package/dist/stores/schema-migrations.d.ts +19 -0
- package/dist/stores/schema-migrations.js +69 -0
- package/dist/types/step-execution-data.d.ts +2 -1
- package/dist/types/validated/execution.d.ts +15 -6
- package/dist/types/validated/step-definition.d.ts +9 -6
- package/dist/types/validated/step-definition.js +4 -4
- package/dist/types/validated/step-outcome.d.ts +10 -0
- package/dist/types/validated/step-outcome.js +7 -2
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
exports.PrettyLogger = exports.runCli = exports.buildDatabaseExecutor = exports.buildInMemoryExecutor = exports.buildInMemoryRunStore = exports.buildDatabaseRunStore = void 0;
|
|
6
|
+
exports.InMemoryStore = exports.SchemaResolver = exports.SchemaCache = exports.validateSecrets = exports.Runner = exports.ExecutorHttpServer = exports.ForestServerWorkflowPort = exports.AgentClientAgentPort = exports.GuidanceStepExecutor = exports.McpStepExecutor = exports.LoadRelatedRecordStepExecutor = exports.TriggerRecordActionStepExecutor = exports.UpdateRecordStepExecutor = exports.ReadRecordStepExecutor = exports.ConditionStepExecutor = exports.BaseStepExecutor = exports.DomainValidationError = exports.InvalidStepDefinitionError = exports.UnsupportedActionFormError = exports.UnsupportedStepTypeError = exports.InvalidPreRecordedArgsError = exports.ExecutorEncryptionKeyMissingError = exports.ConfigurationError = exports.AgentProbeError = exports.AiModelPortError = exports.RunStorePortError = exports.WorkflowPortError = exports.AgentPortError = exports.McpToolInvocationError = exports.McpToolNotFoundError = exports.NoMcpToolsError = exports.StepStateError = exports.ActionNotFoundError = exports.FieldNotFoundError = exports.RelationNotFoundError = exports.InvalidAiRequestError = exports.InvalidAIResponseError = exports.RelatedRecordNotFoundError = exports.NoRelationshipFieldsError = exports.NoActionsError = exports.NoWritableFieldsError = exports.NoResolvedFieldsError = exports.NoReadableFieldsError = exports.NoRecordsError = exports.RecordNotFoundError = exports.MalformedToolCallError = exports.MissingToolCallError = exports.WorkflowExecutorError = exports.ConsoleLogger = exports.StepType = void 0;
|
|
7
|
+
exports.PrettyLogger = exports.runCli = exports.buildDatabaseExecutor = exports.buildInMemoryExecutor = exports.buildInMemoryRunStore = exports.buildDatabaseRunStore = exports.CredentialEncryption = exports.InMemoryMcpOAuthCredentialsStore = exports.DatabaseMcpOAuthCredentialsStore = exports.DatabaseStore = void 0;
|
|
8
8
|
var step_definition_1 = require("./types/validated/step-definition");
|
|
9
9
|
Object.defineProperty(exports, "StepType", { enumerable: true, get: function () { return step_definition_1.StepType; } });
|
|
10
10
|
var console_logger_1 = require("./adapters/console-logger");
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "RunStorePortError", { enumerable: true, get: fun
|
|
|
36
36
|
Object.defineProperty(exports, "AiModelPortError", { enumerable: true, get: function () { return errors_1.AiModelPortError; } });
|
|
37
37
|
Object.defineProperty(exports, "AgentProbeError", { enumerable: true, get: function () { return errors_1.AgentProbeError; } });
|
|
38
38
|
Object.defineProperty(exports, "ConfigurationError", { enumerable: true, get: function () { return errors_1.ConfigurationError; } });
|
|
39
|
+
Object.defineProperty(exports, "ExecutorEncryptionKeyMissingError", { enumerable: true, get: function () { return errors_1.ExecutorEncryptionKeyMissingError; } });
|
|
39
40
|
Object.defineProperty(exports, "InvalidPreRecordedArgsError", { enumerable: true, get: function () { return errors_1.InvalidPreRecordedArgsError; } });
|
|
40
41
|
Object.defineProperty(exports, "UnsupportedStepTypeError", { enumerable: true, get: function () { return errors_1.UnsupportedStepTypeError; } });
|
|
41
42
|
Object.defineProperty(exports, "UnsupportedActionFormError", { enumerable: true, get: function () { return errors_1.UnsupportedActionFormError; } });
|
|
@@ -75,6 +76,12 @@ var in_memory_store_1 = require("./stores/in-memory-store");
|
|
|
75
76
|
Object.defineProperty(exports, "InMemoryStore", { enumerable: true, get: function () { return __importDefault(in_memory_store_1).default; } });
|
|
76
77
|
var database_store_1 = require("./stores/database-store");
|
|
77
78
|
Object.defineProperty(exports, "DatabaseStore", { enumerable: true, get: function () { return __importDefault(database_store_1).default; } });
|
|
79
|
+
var database_mcp_oauth_credentials_store_1 = require("./stores/database-mcp-oauth-credentials-store");
|
|
80
|
+
Object.defineProperty(exports, "DatabaseMcpOAuthCredentialsStore", { enumerable: true, get: function () { return __importDefault(database_mcp_oauth_credentials_store_1).default; } });
|
|
81
|
+
var in_memory_mcp_oauth_credentials_store_1 = require("./stores/in-memory-mcp-oauth-credentials-store");
|
|
82
|
+
Object.defineProperty(exports, "InMemoryMcpOAuthCredentialsStore", { enumerable: true, get: function () { return __importDefault(in_memory_mcp_oauth_credentials_store_1).default; } });
|
|
83
|
+
var credential_encryption_1 = require("./crypto/credential-encryption");
|
|
84
|
+
Object.defineProperty(exports, "CredentialEncryption", { enumerable: true, get: function () { return __importDefault(credential_encryption_1).default; } });
|
|
78
85
|
var build_run_store_1 = require("./stores/build-run-store");
|
|
79
86
|
Object.defineProperty(exports, "buildDatabaseRunStore", { enumerable: true, get: function () { return build_run_store_1.buildDatabaseRunStore; } });
|
|
80
87
|
Object.defineProperty(exports, "buildInMemoryRunStore", { enumerable: true, get: function () { return build_run_store_1.buildInMemoryRunStore; } });
|
|
@@ -85,4 +92,4 @@ var cli_core_1 = require("./cli-core");
|
|
|
85
92
|
Object.defineProperty(exports, "runCli", { enumerable: true, get: function () { return cli_core_1.runCli; } });
|
|
86
93
|
var pretty_logger_1 = require("./adapters/pretty-logger");
|
|
87
94
|
Object.defineProperty(exports, "PrettyLogger", { enumerable: true, get: function () { return __importDefault(pretty_logger_1).default; } });
|
|
88
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
95
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7OztBQUFBLHFFQUE2RDtBQUFwRCwyR0FBQSxRQUFRLE9BQUE7QUF5RWpCLDREQUFxRTtBQUE1RCxnSUFBQSxPQUFPLE9BQWlCO0FBRWpDLG1DQWlDa0I7QUFoQ2hCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLDhHQUFBLG9CQUFvQixPQUFBO0FBQ3BCLGdIQUFBLHNCQUFzQixPQUFBO0FBQ3RCLDZHQUFBLG1CQUFtQixPQUFBO0FBQ25CLHdHQUFBLGNBQWMsT0FBQTtBQUNkLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLHdHQUFBLGNBQWMsT0FBQTtBQUNkLG1IQUFBLHlCQUF5QixPQUFBO0FBQ3pCLG9IQUFBLDBCQUEwQixPQUFBO0FBQzFCLGdIQUFBLHNCQUFzQixPQUFBO0FBQ3RCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLDRHQUFBLGtCQUFrQixPQUFBO0FBQ2xCLDZHQUFBLG1CQUFtQixPQUFBO0FBQ25CLHdHQUFBLGNBQWMsT0FBQTtBQUNkLHlHQUFBLGVBQWUsT0FBQTtBQUNmLDhHQUFBLG9CQUFvQixPQUFBO0FBQ3BCLGdIQUFBLHNCQUFzQixPQUFBO0FBQ3RCLHdHQUFBLGNBQWMsT0FBQTtBQUNkLDJHQUFBLGlCQUFpQixPQUFBO0FBQ2pCLDJHQUFBLGlCQUFpQixPQUFBO0FBQ2pCLDBHQUFBLGdCQUFnQixPQUFBO0FBQ2hCLHlHQUFBLGVBQWUsT0FBQTtBQUNmLDRHQUFBLGtCQUFrQixPQUFBO0FBQ2xCLDJIQUFBLGlDQUFpQyxPQUFBO0FBQ2pDLHFIQUFBLDJCQUEyQixPQUFBO0FBQzNCLGtIQUFBLHdCQUF3QixPQUFBO0FBQ3hCLG9IQUFBLDBCQUEwQixPQUFBO0FBQzFCLG9IQUFBLDBCQUEwQixPQUFBO0FBQzFCLCtHQUFBLHFCQUFxQixPQUFBO0FBRXZCLHFFQUE2RTtBQUFwRSx1SUFBQSxPQUFPLE9BQW9CO0FBQ3BDLCtFQUF1RjtBQUE5RSxpSkFBQSxPQUFPLE9BQXlCO0FBQ3pDLG1GQUEwRjtBQUFqRixvSkFBQSxPQUFPLE9BQTBCO0FBQzFDLHVGQUE4RjtBQUFyRix3SkFBQSxPQUFPLE9BQTRCO0FBQzVDLHVHQUE2RztBQUFwRyx1S0FBQSxPQUFPLE9BQW1DO0FBQ25ELG1HQUF5RztBQUFoRyxtS0FBQSxPQUFPLE9BQWlDO0FBQ2pELG1FQUEyRTtBQUFsRSxxSUFBQSxPQUFPLE9BQW1CO0FBQ25DLDZFQUFxRjtBQUE1RSwrSUFBQSxPQUFPLE9BQXdCO0FBQ3hDLDhFQUFxRjtBQUE1RSxnSkFBQSxPQUFPLE9BQXdCO0FBQ3hDLHNGQUE2RjtBQUFwRix3SkFBQSxPQUFPLE9BQTRCO0FBQzVDLG9FQUE0RTtBQUFuRSwySUFBQSxPQUFPLE9BQXNCO0FBRXRDLG1DQUE2QztBQUFwQyxpSEFBQSxPQUFPLE9BQVU7QUFFMUIsdURBQWdFO0FBQXZELG9JQUFBLE9BQU8sT0FBbUI7QUFDbkMsK0NBQXdEO0FBQS9DLDRIQUFBLE9BQU8sT0FBZTtBQUMvQixxREFBOEQ7QUFBckQsa0lBQUEsT0FBTyxPQUFrQjtBQUNsQyw0REFBb0U7QUFBM0QsaUlBQUEsT0FBTyxPQUFpQjtBQUNqQywwREFBbUU7QUFBMUQsZ0lBQUEsT0FBTyxPQUFpQjtBQUVqQyxzR0FBNEc7QUFBbkcseUtBQUEsT0FBTyxPQUFvQztBQUVwRCx3R0FBNkc7QUFBcEcsMEtBQUEsT0FBTyxPQUFvQztBQU1wRCx3RUFBaUY7QUFBeEUsOElBQUEsT0FBTyxPQUF3QjtBQUV4Qyw0REFBd0Y7QUFBL0Usd0hBQUEscUJBQXFCLE9BQUE7QUFBRSx3SEFBQSxxQkFBcUIsT0FBQTtBQUNyRCxxRUFBeUY7QUFBaEYsZ0lBQUEscUJBQXFCLE9BQUE7QUFBRSxnSUFBQSxxQkFBcUIsT0FBQTtBQUNyRCx1Q0FBb0M7QUFBM0Isa0dBQUEsTUFBTSxPQUFBO0FBQ2YsMERBQW1FO0FBQTFELDhIQUFBLE9BQU8sT0FBZ0IifQ==
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Serializes async work per key: callers sharing a key run one at a time (FIFO), while different
|
|
4
|
+
// keys proceed in parallel. Used to collapse concurrent token refreshes for the same (user, server)
|
|
5
|
+
// into a single in-flight refresh within one process.
|
|
6
|
+
class KeyedMutex {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.tails = new Map();
|
|
9
|
+
}
|
|
10
|
+
async runExclusive(key, task) {
|
|
11
|
+
const previous = this.tails.get(key) ?? Promise.resolve();
|
|
12
|
+
const result = previous.then(() => task());
|
|
13
|
+
// Store a non-rejecting tail so the next caller chains regardless of this task's outcome.
|
|
14
|
+
const tail = result.then(() => undefined, () => undefined);
|
|
15
|
+
this.tails.set(key, tail);
|
|
16
|
+
try {
|
|
17
|
+
return await result;
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
// Drop the entry once this was the last queued task for the key, so the map can't grow.
|
|
21
|
+
if (this.tails.get(key) === tail)
|
|
22
|
+
this.tails.delete(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.default = KeyedMutex;
|
|
27
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoia2V5ZWQtbXV0ZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvb2F1dGgva2V5ZWQtbXV0ZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxpR0FBaUc7QUFDakcsb0dBQW9HO0FBQ3BHLHNEQUFzRDtBQUN0RCxNQUFxQixVQUFVO0lBQS9CO1FBQ21CLFVBQUssR0FBRyxJQUFJLEdBQUcsRUFBNEIsQ0FBQztJQW1CL0QsQ0FBQztJQWpCQyxLQUFLLENBQUMsWUFBWSxDQUFJLEdBQVcsRUFBRSxJQUFzQjtRQUN2RCxNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsSUFBSSxPQUFPLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDMUQsTUFBTSxNQUFNLEdBQUcsUUFBUSxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO1FBQzNDLDBGQUEwRjtRQUMxRixNQUFNLElBQUksR0FBRyxNQUFNLENBQUMsSUFBSSxDQUN0QixHQUFHLEVBQUUsQ0FBQyxTQUFTLEVBQ2YsR0FBRyxFQUFFLENBQUMsU0FBUyxDQUNoQixDQUFDO1FBQ0YsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLElBQUksQ0FBQyxDQUFDO1FBRTFCLElBQUksQ0FBQztZQUNILE9BQU8sTUFBTSxNQUFNLENBQUM7UUFDdEIsQ0FBQztnQkFBUyxDQUFDO1lBQ1Qsd0ZBQXdGO1lBQ3hGLElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEtBQUssSUFBSTtnQkFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUMzRCxDQUFDO0lBQ0gsQ0FBQztDQUNGO0FBcEJELDZCQW9CQyJ9
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface RefreshGrantParams {
|
|
2
|
+
tokenEndpoint: string;
|
|
3
|
+
refreshToken: string;
|
|
4
|
+
clientId?: string | null;
|
|
5
|
+
clientSecret?: string | null;
|
|
6
|
+
tokenEndpointAuthMethod?: string | null;
|
|
7
|
+
scopes?: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface RefreshGrantResult {
|
|
10
|
+
accessToken: string;
|
|
11
|
+
expiresInS?: number;
|
|
12
|
+
refreshToken?: string;
|
|
13
|
+
}
|
|
14
|
+
export default function refreshAccessToken(params: RefreshGrantParams): Promise<RefreshGrantResult>;
|
|
15
|
+
//# sourceMappingURL=refresh-grant.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = refreshAccessToken;
|
|
7
|
+
const errors_1 = require("../errors");
|
|
8
|
+
const token_endpoint_url_1 = __importDefault(require("./token-endpoint-url"));
|
|
9
|
+
// RFC 6749 §2.3.1 form-url-encodes the client id/secret before base64 (space → '+', unlike
|
|
10
|
+
// encodeURIComponent's '%20'); identical for the base64url/hex credentials providers actually issue.
|
|
11
|
+
function formUrlEncode(value) {
|
|
12
|
+
const params = new URLSearchParams();
|
|
13
|
+
params.set('v', value);
|
|
14
|
+
return params.toString().slice('v='.length);
|
|
15
|
+
}
|
|
16
|
+
// Some providers return expires_in as a numeric string; coerce it so the token is still cached
|
|
17
|
+
// (an unusable value leaves it uncached, forcing a full refresh — and rotation — on every call).
|
|
18
|
+
function coerceExpiresInS(value) {
|
|
19
|
+
if (typeof value === 'number')
|
|
20
|
+
return value;
|
|
21
|
+
if (typeof value === 'string' && /^\d+$/.test(value))
|
|
22
|
+
return Number(value);
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function buildRequest(params) {
|
|
26
|
+
const headers = {
|
|
27
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
28
|
+
accept: 'application/json',
|
|
29
|
+
};
|
|
30
|
+
const body = new URLSearchParams({
|
|
31
|
+
grant_type: 'refresh_token',
|
|
32
|
+
refresh_token: params.refreshToken,
|
|
33
|
+
});
|
|
34
|
+
if (params.scopes)
|
|
35
|
+
body.set('scope', params.scopes);
|
|
36
|
+
const { clientId, clientSecret, tokenEndpointAuthMethod } = params;
|
|
37
|
+
if (clientSecret) {
|
|
38
|
+
if (tokenEndpointAuthMethod === 'client_secret_post') {
|
|
39
|
+
if (clientId)
|
|
40
|
+
body.set('client_id', clientId);
|
|
41
|
+
body.set('client_secret', clientSecret);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const credentials = `${formUrlEncode(clientId ?? '')}:${formUrlEncode(clientSecret)}`;
|
|
45
|
+
headers.authorization = `Basic ${Buffer.from(credentials).toString('base64')}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (clientId) {
|
|
49
|
+
body.set('client_id', clientId);
|
|
50
|
+
}
|
|
51
|
+
return { headers, body };
|
|
52
|
+
}
|
|
53
|
+
// Runs the OAuth2 refresh-token grant (RFC 6749 §6) against the stored token endpoint. Distinguishes
|
|
54
|
+
// a non-retryable invalid_grant (revoked / rotated) from a transient failure so the caller can decide
|
|
55
|
+
// between forcing re-auth and retrying.
|
|
56
|
+
async function refreshAccessToken(params) {
|
|
57
|
+
// Defense in depth: deposit validation rejects SSRF-prone endpoints, but a row predating that
|
|
58
|
+
// validation could still carry one — re-check before the outbound POST. Throws (terminal) rather
|
|
59
|
+
// than reaching the network.
|
|
60
|
+
(0, token_endpoint_url_1.default)(params.tokenEndpoint);
|
|
61
|
+
const { headers, body } = buildRequest(params);
|
|
62
|
+
let response;
|
|
63
|
+
try {
|
|
64
|
+
// Never follow redirects: a 3xx to an internal host would re-send the grant body (refresh
|
|
65
|
+
// token, plus the client secret in client_secret_post) there and parse its reply as a token —
|
|
66
|
+
// bypassing the endpoint validation above. Any redirect is treated as a failure.
|
|
67
|
+
response = await fetch(params.tokenEndpoint, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers,
|
|
70
|
+
body,
|
|
71
|
+
redirect: 'manual',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
throw new errors_1.OAuthRefreshError('the token endpoint could not be reached', cause);
|
|
76
|
+
}
|
|
77
|
+
if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400)) {
|
|
78
|
+
throw new errors_1.OAuthRefreshError('the token endpoint attempted a redirect');
|
|
79
|
+
}
|
|
80
|
+
let payload = {};
|
|
81
|
+
try {
|
|
82
|
+
const parsed = await response.json();
|
|
83
|
+
// Ignore a non-object body (e.g. literal null), keeping the {} default so the status checks
|
|
84
|
+
// below still surface a typed OAuthRefreshError.
|
|
85
|
+
if (parsed && typeof parsed === 'object')
|
|
86
|
+
payload = parsed;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Non-JSON body — handled by the status checks below.
|
|
90
|
+
}
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
// invalid_client / unauthorized_client mean the client credential itself is dead (e.g. an expired
|
|
93
|
+
// secret) — non-retryable like invalid_grant, so route to re-consent instead of a doomed retry.
|
|
94
|
+
if (payload.error === 'invalid_grant' ||
|
|
95
|
+
payload.error === 'invalid_client' ||
|
|
96
|
+
payload.error === 'unauthorized_client') {
|
|
97
|
+
throw new errors_1.OAuthInvalidGrantError(typeof payload.error_description === 'string' ? payload.error_description : undefined);
|
|
98
|
+
}
|
|
99
|
+
throw new errors_1.OAuthRefreshError(typeof payload.error === 'string'
|
|
100
|
+
? payload.error
|
|
101
|
+
: `the token endpoint returned ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
if (typeof payload.access_token !== 'string' || !payload.access_token) {
|
|
104
|
+
throw new errors_1.OAuthRefreshError('the token endpoint response had no access_token');
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
accessToken: payload.access_token,
|
|
108
|
+
expiresInS: coerceExpiresInS(payload.expires_in),
|
|
109
|
+
refreshToken: typeof payload.refresh_token === 'string' ? payload.refresh_token : undefined,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVmcmVzaC1ncmFudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9vYXV0aC9yZWZyZXNoLWdyYW50LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBZ0ZBLHFDQXNFQztBQXRKRCxzQ0FBc0U7QUFDdEUsOEVBQTJEO0FBMEIzRCwyRkFBMkY7QUFDM0YscUdBQXFHO0FBQ3JHLFNBQVMsYUFBYSxDQUFDLEtBQWE7SUFDbEMsTUFBTSxNQUFNLEdBQUcsSUFBSSxlQUFlLEVBQUUsQ0FBQztJQUNyQyxNQUFNLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxLQUFLLENBQUMsQ0FBQztJQUV2QixPQUFPLE1BQU0sQ0FBQyxRQUFRLEVBQUUsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0FBQzlDLENBQUM7QUFFRCwrRkFBK0Y7QUFDL0YsaUdBQWlHO0FBQ2pHLFNBQVMsZ0JBQWdCLENBQUMsS0FBYztJQUN0QyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVE7UUFBRSxPQUFPLEtBQUssQ0FBQztJQUM1QyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQztRQUFFLE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBRTNFLE9BQU8sU0FBUyxDQUFDO0FBQ25CLENBQUM7QUFFRCxTQUFTLFlBQVksQ0FBQyxNQUEwQjtJQUk5QyxNQUFNLE9BQU8sR0FBMkI7UUFDdEMsY0FBYyxFQUFFLG1DQUFtQztRQUNuRCxNQUFNLEVBQUUsa0JBQWtCO0tBQzNCLENBQUM7SUFDRixNQUFNLElBQUksR0FBRyxJQUFJLGVBQWUsQ0FBQztRQUMvQixVQUFVLEVBQUUsZUFBZTtRQUMzQixhQUFhLEVBQUUsTUFBTSxDQUFDLFlBQVk7S0FDbkMsQ0FBQyxDQUFDO0lBRUgsSUFBSSxNQUFNLENBQUMsTUFBTTtRQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFF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
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = assertSafeTokenEndpoint;
|
|
7
|
+
const net_1 = __importDefault(require("net"));
|
|
8
|
+
const errors_1 = require("../errors");
|
|
9
|
+
// The token endpoint comes from the deposit body, and the executor POSTs the refresh grant (with
|
|
10
|
+
// client credentials) to it — so an unconstrained value is an SSRF vector. The guard rejects hosts
|
|
11
|
+
// that are never a legitimate endpoint but are prime SSRF targets (loopback, link-local /
|
|
12
|
+
// cloud-metadata) and requires TLS, while deliberately allowing private (RFC1918) hosts: a private
|
|
13
|
+
// OAuth provider reached over the internal network is a supported deployment, so blocking it would
|
|
14
|
+
// break a real use case. http and loopback are relaxed only when NODE_ENV explicitly opts in
|
|
15
|
+
// (development/test) — see isRelaxedEnv. Only IP literals are inspected — resolving a hostname here
|
|
16
|
+
// would be racy (it can resolve to a different address at fetch time), not safer.
|
|
17
|
+
// Fail closed: the strict rules (https + no loopback) apply everywhere UNLESS NODE_ENV explicitly
|
|
18
|
+
// opts into the local-dev relaxation. An unset or misspelled NODE_ENV stays strict, so a
|
|
19
|
+
// misconfigured deploy can't silently allow loopback targets or cleartext http.
|
|
20
|
+
function isRelaxedEnv() {
|
|
21
|
+
return process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test';
|
|
22
|
+
}
|
|
23
|
+
function unbracket(hostname) {
|
|
24
|
+
return hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
|
|
25
|
+
}
|
|
26
|
+
function classifyIpv4(host) {
|
|
27
|
+
const [a, b] = host.split('.').map(Number);
|
|
28
|
+
return {
|
|
29
|
+
loopback: a === 127, // 127.0.0.0/8
|
|
30
|
+
linkLocal: a === 169 && b === 254, // 169.254.0.0/16
|
|
31
|
+
unspecified: a === 0, // 0.0.0.0/8 — "this host"; connects to loopback on Linux
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Returns the embedded IPv4 of an IPv4-mapped IPv6 address (otherwise null). The WHATWG URL parser
|
|
35
|
+
// normalizes `::ffff:127.0.0.1` to the hex form `::ffff:7f00:1`, which would otherwise dodge the
|
|
36
|
+
// IPv4 loopback/link-local checks and still reach the executor's loopback / metadata interface.
|
|
37
|
+
function embeddedIpv4(host) {
|
|
38
|
+
const tail = host.toLowerCase().match(/^::ffff:(.+)$/)?.[1];
|
|
39
|
+
if (!tail)
|
|
40
|
+
return null;
|
|
41
|
+
if (net_1.default.isIPv4(tail))
|
|
42
|
+
return tail;
|
|
43
|
+
const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
44
|
+
if (!hex)
|
|
45
|
+
return null;
|
|
46
|
+
const hi = parseInt(hex[1], 16); // high 16 bits → first two octets
|
|
47
|
+
const lo = parseInt(hex[2], 16); // low 16 bits → last two octets
|
|
48
|
+
return `${Math.floor(hi / 256)}.${hi % 256}.${Math.floor(lo / 256)}.${lo % 256}`;
|
|
49
|
+
}
|
|
50
|
+
function classify(host) {
|
|
51
|
+
const kind = net_1.default.isIP(host);
|
|
52
|
+
if (kind === 4)
|
|
53
|
+
return classifyIpv4(host);
|
|
54
|
+
if (kind === 6) {
|
|
55
|
+
const mapped = embeddedIpv4(host);
|
|
56
|
+
if (mapped)
|
|
57
|
+
return classifyIpv4(mapped);
|
|
58
|
+
const normalized = host.toLowerCase().split('%')[0]; // drop any zone id
|
|
59
|
+
const firstHextet = normalized.split(':')[0];
|
|
60
|
+
return {
|
|
61
|
+
loopback: normalized === '::1' || normalized === '0:0:0:0:0:0:0:1',
|
|
62
|
+
linkLocal: /^fe[89ab]/.test(firstHextet), // fe80::/10
|
|
63
|
+
unspecified: normalized === '::' || normalized === '0:0:0:0:0:0:0:0',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Not an IP literal. Reject the reserved loopback names — bare `localhost`, the `.localhost` TLD
|
|
67
|
+
// (RFC 6761), and their trailing-dot FQDN forms — since they resolve to loopback. Other hostnames
|
|
68
|
+
// are left to the scheme rule; their DNS is not resolved here (a hostname pointed at an internal
|
|
69
|
+
// IP is the filtering-agent's job, not this string check).
|
|
70
|
+
return {
|
|
71
|
+
loopback: /^localhost\.?$|\.localhost\.?$/.test(host.toLowerCase()),
|
|
72
|
+
linkLocal: false,
|
|
73
|
+
unspecified: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function assertSafeTokenEndpoint(raw) {
|
|
77
|
+
let url;
|
|
78
|
+
try {
|
|
79
|
+
url = new URL(raw);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
throw new errors_1.InvalidTokenEndpointError('it is not a valid absolute URL');
|
|
83
|
+
}
|
|
84
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
85
|
+
throw new errors_1.InvalidTokenEndpointError('it must use http or https');
|
|
86
|
+
}
|
|
87
|
+
const { loopback, linkLocal, unspecified } = classify(unbracket(url.hostname));
|
|
88
|
+
if (unspecified) {
|
|
89
|
+
throw new errors_1.InvalidTokenEndpointError('it points at the unspecified address (0.0.0.0 / ::)');
|
|
90
|
+
}
|
|
91
|
+
if (linkLocal) {
|
|
92
|
+
throw new errors_1.InvalidTokenEndpointError('it points at a link-local / metadata address');
|
|
93
|
+
}
|
|
94
|
+
if (!isRelaxedEnv()) {
|
|
95
|
+
if (url.protocol !== 'https:') {
|
|
96
|
+
throw new errors_1.InvalidTokenEndpointError('it must use https');
|
|
97
|
+
}
|
|
98
|
+
if (loopback) {
|
|
99
|
+
throw new errors_1.InvalidTokenEndpointError('it points at the executor host (loopback)');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9rZW4tZW5kcG9pbnQtdXJsLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL29hdXRoL3Rva2VuLWVuZHBvaW50LXVybC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQW9GQSwwQ0FnQ0M7QUFwSEQsOENBQXNCO0FBRXRCLHNDQUFzRDtBQUV0RCxpR0FBaUc7QUFDakcsbUdBQW1HO0FBQ25HLDBGQUEwRjtBQUMxRixtR0FBbUc7QUFDbkcsbUdBQW1HO0FBQ25HLDZGQUE2RjtBQUM3RixvR0FBb0c7QUFDcEcsa0ZBQWtGO0FBRWxGLGtHQUFrRztBQUNsRyx5RkFBeUY7QUFDekYsZ0ZBQWdGO0FBQ2hGLFNBQVMsWUFBWTtJQUNuQixPQUFPLE9BQU8sQ0FBQyxHQUFHLENBQUMsUUFBUSxLQUFLLGFBQWEsSUFBSSxPQUFPLENBQUMsR0FBRyxDQUFDLFFBQVEsS0FBSyxNQUFNLENBQUM7QUFDbkYsQ0FBQztBQUVELFNBQVMsU0FBUyxDQUFDLFFBQWdCO0lBQ2pDLE9BQU8sUUFBUSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsSUFBSSxRQUFRLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUM7QUFDL0YsQ0FBQztBQUlELFNBQVMsWUFBWSxDQUFDLElBQVk7SUFDaEMsTUFBTSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUUzQyxPQUFPO1FBQ0wsUUFBUSxFQUFFLENBQUMsS0FBSyxHQUFHLEVBQUUsY0FBYztRQUNuQyxTQUFTLEVBQUUsQ0FBQyxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssR0FBRyxFQUFFLGlCQUFpQjtRQUNwRCxXQUFXLEVBQUUsQ0FBQyxL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
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RefreshGrantParams, RefreshGrantResult } from './refresh-grant';
|
|
2
|
+
import type CredentialEncryption from '../crypto/credential-encryption';
|
|
3
|
+
import type { Logger } from '../ports/logger-port';
|
|
4
|
+
import type { McpOAuthCredentialsStore } from '../ports/mcp-oauth-credentials-store';
|
|
5
|
+
export interface OAuthTokenServiceOptions {
|
|
6
|
+
store: McpOAuthCredentialsStore;
|
|
7
|
+
encryption: CredentialEncryption;
|
|
8
|
+
logger?: Logger;
|
|
9
|
+
expirySkewS?: number;
|
|
10
|
+
refreshAccessToken?: (params: RefreshGrantParams) => Promise<RefreshGrantResult>;
|
|
11
|
+
now?: () => number;
|
|
12
|
+
}
|
|
13
|
+
export default class OAuthTokenService {
|
|
14
|
+
private readonly store;
|
|
15
|
+
private readonly encryption;
|
|
16
|
+
private readonly logger?;
|
|
17
|
+
private readonly expirySkewMs;
|
|
18
|
+
private readonly refreshAccessToken;
|
|
19
|
+
private readonly now;
|
|
20
|
+
private readonly cache;
|
|
21
|
+
private readonly mutex;
|
|
22
|
+
private readonly evictionEpoch;
|
|
23
|
+
constructor(options: OAuthTokenServiceOptions);
|
|
24
|
+
getAccessToken(userId: number, mcpServerId: string, options?: {
|
|
25
|
+
forceRefresh?: boolean;
|
|
26
|
+
}): Promise<string>;
|
|
27
|
+
evict(userId: number, mcpServerId: string): void;
|
|
28
|
+
private readCache;
|
|
29
|
+
private refreshAndCache;
|
|
30
|
+
private runGrantWithRotationRetry;
|
|
31
|
+
private toGrantParams;
|
|
32
|
+
private persistRotatedRefreshToken;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=token-service.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const defaults_1 = require("../defaults");
|
|
7
|
+
const errors_1 = require("../errors");
|
|
8
|
+
const keyed_mutex_1 = __importDefault(require("./keyed-mutex"));
|
|
9
|
+
const refresh_grant_1 = __importDefault(require("./refresh-grant"));
|
|
10
|
+
// Acquires an MCP OAuth access token for a (user, server): serves a cached token until it nears
|
|
11
|
+
// expiry, otherwise runs the refresh-token grant under a per-key mutex (one in-flight refresh per
|
|
12
|
+
// user+server in this process) and recovers from a concurrent refresh-token rotation via a single
|
|
13
|
+
// re-read + retry. A missing credential or a genuinely rejected refresh raises
|
|
14
|
+
// OAuthReauthRequiredError so the step pauses for re-authentication.
|
|
15
|
+
class OAuthTokenService {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.cache = new Map();
|
|
18
|
+
this.mutex = new keyed_mutex_1.default();
|
|
19
|
+
// Bumped on evict() so a refresh already in flight for a key can detect a disconnect that landed
|
|
20
|
+
// mid-refresh and skip repopulating the cache for the now-deleted credential.
|
|
21
|
+
this.evictionEpoch = new Map();
|
|
22
|
+
this.store = options.store;
|
|
23
|
+
this.encryption = options.encryption;
|
|
24
|
+
this.logger = options.logger;
|
|
25
|
+
this.expirySkewMs = (options.expirySkewS ?? defaults_1.DEFAULT_OAUTH_EXPIRY_SKEW_S) * 1000;
|
|
26
|
+
this.refreshAccessToken = options.refreshAccessToken ?? refresh_grant_1.default;
|
|
27
|
+
this.now = options.now ?? Date.now;
|
|
28
|
+
}
|
|
29
|
+
async getAccessToken(userId, mcpServerId, options) {
|
|
30
|
+
const key = `${userId}:${mcpServerId}`;
|
|
31
|
+
const forceRefresh = options?.forceRefresh ?? false;
|
|
32
|
+
if (!forceRefresh) {
|
|
33
|
+
const cached = this.readCache(key);
|
|
34
|
+
if (cached)
|
|
35
|
+
return cached;
|
|
36
|
+
}
|
|
37
|
+
return this.mutex.runExclusive(key, async () => {
|
|
38
|
+
// Re-check inside the lock: a concurrent caller may have just refreshed for this key.
|
|
39
|
+
if (!forceRefresh) {
|
|
40
|
+
const cached = this.readCache(key);
|
|
41
|
+
if (cached)
|
|
42
|
+
return cached;
|
|
43
|
+
}
|
|
44
|
+
return this.refreshAndCache(userId, mcpServerId, key);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Drop the cached access token for a (user, server). Called on credential delete so a disconnect
|
|
48
|
+
// takes effect immediately, instead of the executor serving the cached token until it expires.
|
|
49
|
+
evict(userId, mcpServerId) {
|
|
50
|
+
const key = `${userId}:${mcpServerId}`;
|
|
51
|
+
this.cache.delete(key);
|
|
52
|
+
this.evictionEpoch.set(key, (this.evictionEpoch.get(key) ?? 0) + 1);
|
|
53
|
+
}
|
|
54
|
+
readCache(key) {
|
|
55
|
+
const entry = this.cache.get(key);
|
|
56
|
+
if (!entry)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (this.now() >= entry.expiresAtMs - this.expirySkewMs) {
|
|
59
|
+
// Drop expired entries on read so the cache can't grow without bound across many keys.
|
|
60
|
+
this.cache.delete(key);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return entry.accessToken;
|
|
64
|
+
}
|
|
65
|
+
async refreshAndCache(userId, mcpServerId, key) {
|
|
66
|
+
const epochAtStart = this.evictionEpoch.get(key) ?? 0;
|
|
67
|
+
const credential = await this.store.get(userId, mcpServerId);
|
|
68
|
+
if (!credential)
|
|
69
|
+
throw new errors_1.OAuthReauthRequiredError(mcpServerId);
|
|
70
|
+
const { result, credential: grantedCredential } = await this.runGrantWithRotationRetry(credential, userId, mcpServerId);
|
|
71
|
+
const evictedDuringRefresh = (this.evictionEpoch.get(key) ?? 0) !== epochAtStart;
|
|
72
|
+
// Skip caching if the credential was disconnected mid-refresh (would serve a token for a deleted
|
|
73
|
+
// credential) or the grant carried no expiry (never servable); the in-flight caller still gets it.
|
|
74
|
+
if (!evictedDuringRefresh && result.expiresInS !== undefined) {
|
|
75
|
+
this.cache.set(key, {
|
|
76
|
+
accessToken: result.accessToken,
|
|
77
|
+
expiresAtMs: this.now() + result.expiresInS * 1000,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.cache.delete(key);
|
|
82
|
+
}
|
|
83
|
+
if (result.refreshToken) {
|
|
84
|
+
await this.persistRotatedRefreshToken(grantedCredential, result.refreshToken);
|
|
85
|
+
}
|
|
86
|
+
return result.accessToken;
|
|
87
|
+
}
|
|
88
|
+
// On invalid_grant a peer instance likely rotated the refresh token out from under us: re-read the
|
|
89
|
+
// row and retry once with the current token; a second invalid_grant (or an unchanged token) is a
|
|
90
|
+
// genuine revocation and forces re-auth. Returns the credential whose token produced the grant so
|
|
91
|
+
// the caller writes the rotated token back onto that (current) row, not the stale one.
|
|
92
|
+
async runGrantWithRotationRetry(credential, userId, mcpServerId) {
|
|
93
|
+
try {
|
|
94
|
+
const result = await this.refreshAccessToken(this.toGrantParams(credential));
|
|
95
|
+
return { result, credential };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (!(error instanceof errors_1.OAuthInvalidGrantError))
|
|
99
|
+
throw error;
|
|
100
|
+
// A peer rotated the refresh token iff the stored value changed since we read it.
|
|
101
|
+
const latest = await this.store.get(userId, mcpServerId);
|
|
102
|
+
const wasRotated = latest !== null &&
|
|
103
|
+
latest.refreshTokenEnc.toString('base64') !== credential.refreshTokenEnc.toString('base64');
|
|
104
|
+
if (!latest || !wasRotated) {
|
|
105
|
+
throw new errors_1.OAuthReauthRequiredError(mcpServerId);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const result = await this.refreshAccessToken(this.toGrantParams(latest));
|
|
109
|
+
return { result, credential: latest };
|
|
110
|
+
}
|
|
111
|
+
catch (retryError) {
|
|
112
|
+
if (retryError instanceof errors_1.OAuthInvalidGrantError) {
|
|
113
|
+
throw new errors_1.OAuthReauthRequiredError(mcpServerId);
|
|
114
|
+
}
|
|
115
|
+
throw retryError;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Decrypt happens here. A decrypt failure with the key PRESENT (auth-tag mismatch — the row was
|
|
120
|
+
// encrypted under a since-rotated/hard-swapped key, or is corrupt) is recoverable: re-consent
|
|
121
|
+
// re-deposits under the current key, so surface it as needs-oauth-reauth. A missing key
|
|
122
|
+
// (ExecutorEncryptionKeyMissingError) is an operator misconfig, not re-consent-resolvable, so it
|
|
123
|
+
// propagates as a terminal error (a re-deposit would just 503 at the deposit endpoint).
|
|
124
|
+
toGrantParams(credential) {
|
|
125
|
+
try {
|
|
126
|
+
return {
|
|
127
|
+
tokenEndpoint: credential.tokenEndpoint,
|
|
128
|
+
refreshToken: this.encryption.decrypt(credential.refreshTokenEnc),
|
|
129
|
+
clientId: credential.clientId,
|
|
130
|
+
clientSecret: credential.clientSecretEnc
|
|
131
|
+
? this.encryption.decrypt(credential.clientSecretEnc)
|
|
132
|
+
: null,
|
|
133
|
+
tokenEndpointAuthMethod: credential.tokenEndpointAuthMethod,
|
|
134
|
+
scopes: credential.scopes,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof errors_1.ExecutorEncryptionKeyMissingError)
|
|
139
|
+
throw error;
|
|
140
|
+
throw new errors_1.OAuthReauthRequiredError(credential.mcpServerId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async persistRotatedRefreshToken(credential, refreshToken) {
|
|
144
|
+
try {
|
|
145
|
+
const encrypted = this.encryption.encrypt(refreshToken);
|
|
146
|
+
// Id-scoped so a disconnect (or disconnect + re-authorize) after the grant read leaves an
|
|
147
|
+
// absent or different row — the rotated-token write-back then can't resurrect or clobber it.
|
|
148
|
+
await this.store.updateIfPresent(credential.id, {
|
|
149
|
+
userId: credential.userId,
|
|
150
|
+
mcpServerId: credential.mcpServerId,
|
|
151
|
+
refreshTokenEnc: encrypted.ciphertext,
|
|
152
|
+
clientId: credential.clientId,
|
|
153
|
+
clientSecretEnc: credential.clientSecretEnc,
|
|
154
|
+
clientSecretExpiresAt: credential.clientSecretExpiresAt,
|
|
155
|
+
tokenEndpoint: credential.tokenEndpoint,
|
|
156
|
+
tokenEndpointAuthMethod: credential.tokenEndpointAuthMethod,
|
|
157
|
+
scopes: credential.scopes,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
// A failed write-back is not fatal: the access token just obtained is valid for this call.
|
|
162
|
+
// The rotated refresh token is lost, so a later refresh forces re-authentication — the safe
|
|
163
|
+
// fallback, and better than failing the current operation over a transient write error.
|
|
164
|
+
this.logger?.('Error', 'Failed to persist rotated MCP OAuth refresh token', {
|
|
165
|
+
mcpServerId: credential.mcpServerId,
|
|
166
|
+
error: error instanceof Error ? error.message : String(error),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
exports.default = OAuthTokenService;
|
|
172
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9rZW4tc2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9vYXV0aC90b2tlbi1zZXJ2aWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBUUEsMENBQTBEO0FBQzFELHNDQUltQjtBQUNuQixnRUFBdUM7QUFDdkMsb0VBQXdEO0FBZ0J4RCxnR0FBZ0c7QUFDaEcsa0dBQWtHO0FBQ2xHLGtHQUFrRztBQUNsRywrRUFBK0U7QUFDL0UscUVBQXFFO0FBQ3JFLE1BQXFCLGlCQUFpQjtJQWFwQyxZQUFZLE9BQWlDO1FBTjVCLFVBQUssR0FBRyxJQUFJLEdBQUcsRUFBdUIsQ0FBQztRQUN2QyxVQUFLLEdBQUcsSUFBSSxxQkFBVSxFQUFFLENBQUM7UUFDMUMsaUdBQWlHO1FBQ2pHLDhFQUE4RTtRQUM3RCxrQkFBYSxHQUFHLElBQUksR0FBRyxFQUFrQixDQUFDO1FBR3pELElBQUksQ0FBQyxLQUFLLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQztRQUMzQixJQUFJLENBQUMsVUFBVSxHQUFHLE9BQU8sQ0FBQyxVQUFVLENBQUM7UUFDckMsSUFBSSxDQUFDLE1BQU0sR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDO1FBQzdCLElBQUksQ0FBQyxZQUFZLEdBQUcsQ0FBQyxPQUFPLENBQUMsV0FBVyxJQUFJLHNDQUEyQixDQUFDLEdBQUcsSUFBSSxDQUFDO1FBQ2hGLElBQUksQ0FBQyxrQkFBa0IsR0FBRyxPQUFPLENBQUMsa0JBQWtCLElBQUksdUJBQXlCLENBQUM7UUFDbEYsSUFBSSxDQUFDLEdBQUcsR0FBRyxPQUFPLENBQUMsR0FBRyxJQUFJLElBQUksQ0FBQyxHQUFHLENBQUM7SUFDckMsQ0FBQztJQUVELEtBQUssQ0FBQyxjQUFjLENBQ2xCLE1BQWMsRUFDZCxXQUFtQixFQUNuQixPQUFvQztRQUVwQyxNQUFNLEdBQUcsR0FBRyxHQUFHLE1BQU0sSUFBSSxXQUFXLEVBQUUsQ0FBQztRQUN2QyxNQUFNLFlBQVksR0FB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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BaseChatModel, RemoteTool, ToolConfig } from '@forestadmin/ai-proxy';
|
|
1
|
+
import type { BaseChatModel, McpServerLoadFailure, RemoteTool, ToolConfig } from '@forestadmin/ai-proxy';
|
|
2
2
|
export interface GetModelOptions {
|
|
3
3
|
aiConfigName?: string;
|
|
4
4
|
userId?: number;
|
|
@@ -6,6 +6,10 @@ export interface GetModelOptions {
|
|
|
6
6
|
export interface AiModelPort {
|
|
7
7
|
getModel(options?: GetModelOptions): BaseChatModel;
|
|
8
8
|
loadRemoteTools(configs: Record<string, ToolConfig>): Promise<RemoteTool[]>;
|
|
9
|
+
loadRemoteToolsWithFailures(configs: Record<string, ToolConfig>): Promise<{
|
|
10
|
+
tools: RemoteTool[];
|
|
11
|
+
failures: McpServerLoadFailure[];
|
|
12
|
+
}>;
|
|
9
13
|
closeConnections(): Promise<void>;
|
|
10
14
|
}
|
|
11
15
|
//# sourceMappingURL=ai-model-port.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Logger } from './logger-port';
|
|
2
|
+
export interface McpOAuthCredentialInput {
|
|
3
|
+
userId: number;
|
|
4
|
+
mcpServerId: string;
|
|
5
|
+
refreshTokenEnc: Buffer;
|
|
6
|
+
clientId?: string | null;
|
|
7
|
+
clientSecretEnc?: Buffer | null;
|
|
8
|
+
clientSecretExpiresAt?: Date | null;
|
|
9
|
+
tokenEndpoint: string;
|
|
10
|
+
tokenEndpointAuthMethod?: string | null;
|
|
11
|
+
scopes?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface StoredMcpOAuthCredential extends McpOAuthCredentialInput {
|
|
14
|
+
id: number;
|
|
15
|
+
}
|
|
16
|
+
export interface McpOAuthCredentialsStore {
|
|
17
|
+
init(logger?: Logger): Promise<void>;
|
|
18
|
+
close(logger?: Logger): Promise<void>;
|
|
19
|
+
get(userId: number, mcpServerId: string): Promise<StoredMcpOAuthCredential | null>;
|
|
20
|
+
upsert(credential: McpOAuthCredentialInput): Promise<void>;
|
|
21
|
+
updateIfPresent(id: number, credential: McpOAuthCredentialInput): Promise<void>;
|
|
22
|
+
delete(userId: number, mcpServerId: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=mcp-oauth-credentials-store.d.ts.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWNwLW9hdXRoLWNyZWRlbnRpYWxzLXN0b3JlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3BvcnRzL21jcC1vYXV0aC1jcmVkZW50aWFscy1zdG9yZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIn0=
|
|
@@ -5,5 +5,6 @@ export interface RunStore {
|
|
|
5
5
|
close(logger?: Logger): Promise<void>;
|
|
6
6
|
getStepExecutions(runId: string): Promise<StepExecutionData[]>;
|
|
7
7
|
saveStepExecution(runId: string, stepExecution: StepExecutionData): Promise<void>;
|
|
8
|
+
deleteStepExecution(runId: string, stepIndex: number): Promise<void>;
|
|
8
9
|
}
|
|
9
10
|
//# sourceMappingURL=run-store.d.ts.map
|