@forestadmin/workflow-executor 1.13.0 → 1.14.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.
Files changed (55) hide show
  1. package/README.md +4 -0
  2. package/dist/adapters/ai-client-adapter.d.ts +5 -1
  3. package/dist/adapters/ai-client-adapter.js +4 -1
  4. package/dist/adapters/always-error-ai-model-port.d.ts +5 -1
  5. package/dist/adapters/always-error-ai-model-port.js +6 -1
  6. package/dist/adapters/server-ai-adapter.d.ts +5 -1
  7. package/dist/adapters/server-ai-adapter.js +4 -1
  8. package/dist/adapters/step-outcome-to-update-step-mapper.js +4 -1
  9. package/dist/build-workflow-executor.js +87 -2
  10. package/dist/crypto/credential-encryption.d.ts +10 -0
  11. package/dist/crypto/credential-encryption.js +75 -0
  12. package/dist/defaults.d.ts +1 -0
  13. package/dist/defaults.js +5 -2
  14. package/dist/errors.d.ts +18 -0
  15. package/dist/errors.js +50 -2
  16. package/dist/executors/mcp-step-executor.d.ts +7 -2
  17. package/dist/executors/mcp-step-executor.js +78 -14
  18. package/dist/executors/step-executor-factory.d.ts +1 -1
  19. package/dist/executors/step-executor-factory.js +24 -3
  20. package/dist/http/executor-http-server.d.ts +12 -0
  21. package/dist/http/executor-http-server.js +90 -1
  22. package/dist/http/mcp-oauth-credentials.d.ts +24 -0
  23. package/dist/http/mcp-oauth-credentials.js +77 -0
  24. package/dist/index.d.ts +7 -1
  25. package/dist/index.js +10 -3
  26. package/dist/oauth/keyed-mutex.d.ts +5 -0
  27. package/dist/oauth/keyed-mutex.js +27 -0
  28. package/dist/oauth/refresh-grant.d.ts +15 -0
  29. package/dist/oauth/refresh-grant.js +112 -0
  30. package/dist/oauth/token-endpoint-url.d.ts +2 -0
  31. package/dist/oauth/token-endpoint-url.js +103 -0
  32. package/dist/oauth/token-service.d.ts +34 -0
  33. package/dist/oauth/token-service.js +172 -0
  34. package/dist/ports/ai-model-port.d.ts +5 -1
  35. package/dist/ports/mcp-oauth-credentials-store.d.ts +24 -0
  36. package/dist/ports/mcp-oauth-credentials-store.js +3 -0
  37. package/dist/ports/run-store.d.ts +1 -0
  38. package/dist/remote-tool-fetcher.d.ts +7 -2
  39. package/dist/remote-tool-fetcher.js +53 -6
  40. package/dist/runner.d.ts +2 -0
  41. package/dist/runner.js +3 -3
  42. package/dist/stores/database-mcp-oauth-credentials-store.d.ts +22 -0
  43. package/dist/stores/database-mcp-oauth-credentials-store.js +188 -0
  44. package/dist/stores/database-store.d.ts +1 -1
  45. package/dist/stores/database-store.js +17 -50
  46. package/dist/stores/in-memory-mcp-oauth-credentials-store.d.ts +13 -0
  47. package/dist/stores/in-memory-mcp-oauth-credentials-store.js +44 -0
  48. package/dist/stores/in-memory-store.d.ts +1 -0
  49. package/dist/stores/in-memory-store.js +6 -1
  50. package/dist/stores/schema-migrations.d.ts +19 -0
  51. package/dist/stores/schema-migrations.js +69 -0
  52. package/dist/types/validated/execution.d.ts +6 -0
  53. package/dist/types/validated/step-outcome.d.ts +10 -0
  54. package/dist/types/validated/step-outcome.js +7 -2
  55. 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.DatabaseStore = 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.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 = 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7OztBQUFBLHFFQUE2RDtBQUFwRCwyR0FBQSxRQUFRLE9BQUE7QUF5RWpCLDREQUFxRTtBQUE1RCxnSUFBQSxPQUFPLE9BQWlCO0FBRWpDLG1DQWdDa0I7QUEvQmhCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLDhHQUFBLG9CQUFvQixPQUFBO0FBQ3BCLGdIQUFBLHNCQUFzQixPQUFBO0FBQ3RCLDZHQUFBLG1CQUFtQixPQUFBO0FBQ25CLHdHQUFBLGNBQWMsT0FBQTtBQUNkLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLHdHQUFBLGNBQWMsT0FBQTtBQUNkLG1IQUFBLHlCQUF5QixPQUFBO0FBQ3pCLG9IQUFBLDBCQUEwQixPQUFBO0FBQzFCLGdIQUFBLHNCQUFzQixPQUFBO0FBQ3RCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLDRHQUFBLGtCQUFrQixPQUFBO0FBQ2xCLDZHQUFBLG1CQUFtQixPQUFBO0FBQ25CLHdHQUFBLGNBQWMsT0FBQTtBQUNkLHlHQUFBLGVBQWUsT0FBQTtBQUNmLDhHQUFBLG9CQUFvQixPQUFBO0FBQ3BCLGdIQUFBLHNCQUFzQixPQUFBO0FBQ3RCLHdHQUFBLGNBQWMsT0FBQTtBQUNkLDJHQUFBLGlCQUFpQixPQUFBO0FBQ2pCLDJHQUFBLGlCQUFpQixPQUFBO0FBQ2pCLDBHQUFBLGdCQUFnQixPQUFBO0FBQ2hCLHlHQUFBLGVBQWUsT0FBQTtBQUNmLDRHQUFBLGtCQUFrQixPQUFBO0FBQ2xCLHFIQUFBLDJCQUEyQixPQUFBO0FBQzNCLGtIQUFBLHdCQUF3QixPQUFBO0FBQ3hCLG9IQUFBLDBCQUEwQixPQUFBO0FBQzFCLG9IQUFBLDBCQUEwQixPQUFBO0FBQzFCLCtHQUFBLHFCQUFxQixPQUFBO0FBRXZCLHFFQUE2RTtBQUFwRSx1SUFBQSxPQUFPLE9BQW9CO0FBQ3BDLCtFQUF1RjtBQUE5RSxpSkFBQSxPQUFPLE9BQXlCO0FBQ3pDLG1GQUEwRjtBQUFqRixvSkFBQSxPQUFPLE9BQTBCO0FBQzFDLHVGQUE4RjtBQUFyRix3SkFBQSxPQUFPLE9BQTRCO0FBQzVDLHVHQUE2RztBQUFwRyx1S0FBQSxPQUFPLE9BQW1DO0FBQ25ELG1HQUF5RztBQUFoRyxtS0FBQSxPQUFPLE9BQWlDO0FBQ2pELG1FQUEyRTtBQUFsRSxxSUFBQSxPQUFPLE9BQW1CO0FBQ25DLDZFQUFxRjtBQUE1RSwrSUFBQSxPQUFPLE9BQXdCO0FBQ3hDLDhFQUFxRjtBQUE1RSxnSkFBQSxPQUFPLE9BQXdCO0FBQ3hDLHNGQUE2RjtBQUFwRix3SkFBQSxPQUFPLE9BQTRCO0FBQzVDLG9FQUE0RTtBQUFuRSwySUFBQSxPQUFPLE9BQXNCO0FBRXRDLG1DQUE2QztBQUFwQyxpSEFBQSxPQUFPLE9BQVU7QUFFMUIsdURBQWdFO0FBQXZELG9JQUFBLE9BQU8sT0FBbUI7QUFDbkMsK0NBQXdEO0FBQS9DLDRIQUFBLE9BQU8sT0FBZTtBQUMvQixxREFBOEQ7QUFBckQsa0lBQUEsT0FBTyxPQUFrQjtBQUNsQyw0REFBb0U7QUFBM0QsaUlBQUEsT0FBTyxPQUFpQjtBQUNqQywwREFBbUU7QUFBMUQsZ0lBQUEsT0FBTyxPQUFpQjtBQUVqQyw0REFBd0Y7QUFBL0Usd0hBQUEscUJBQXFCLE9BQUE7QUFBRSx3SEFBQSxxQkFBcUIsT0FBQTtBQUNyRCxxRUFBeUY7QUFBaEYsZ0lBQUEscUJBQXFCLE9BQUE7QUFBRSxnSUFBQSxxQkFBcUIsT0FBQTtBQUNyRCx1Q0FBb0M7QUFBM0Isa0dBQUEsTUFBTSxPQUFBO0FBQ2YsMERBQW1FO0FBQTFELDhIQUFBLE9BQU8sT0FBZ0IifQ==
95
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7OztBQUFBLHFFQUE2RDtBQUFwRCwyR0FBQSxRQUFRLE9BQUE7QUF5RWpCLDREQUFxRTtBQUE1RCxnSUFBQSxPQUFPLE9BQWlCO0FBRWpDLG1DQWlDa0I7QUFoQ2hCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLDhHQUFBLG9CQUFvQixPQUFBO0FBQ3BCLGdIQUFBLHNCQUFzQixPQUFBO0FBQ3RCLDZHQUFBLG1CQUFtQixPQUFBO0FBQ25CLHdHQUFBLGNBQWMsT0FBQTtBQUNkLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLHdHQUFBLGNBQWMsT0FBQTtBQUNkLG1IQUFBLHlCQUF5QixPQUFBO0FBQ3pCLG9IQUFBLDBCQUEwQixPQUFBO0FBQzFCLGdIQUFBLHNCQUFzQixPQUFBO0FBQ3RCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLCtHQUFBLHFCQUFxQixPQUFBO0FBQ3JCLDRHQUFBLGtCQUFrQixPQUFBO0FBQ2xCLDZHQUFBLG1CQUFtQixPQUFBO0FBQ25CLHdHQUFBLGNBQWMsT0FBQTtBQUNkLHlHQUFBLGVBQWUsT0FBQTtBQUNmLDhHQUFBLG9CQUFvQixPQUFBO0FBQ3BCLGdIQUFBLHNCQUFzQixPQUFBO0FBQ3RCLHdHQUFBLGNBQWMsT0FBQTtBQUNkLDJHQUFBLGlCQUFpQixPQUFBO0FBQ2pCLDJHQUFBLGlCQUFpQixPQUFBO0FBQ2pCLDBHQUFBLGdCQUFnQixPQUFBO0FBQ2hCLHlHQUFBLGVBQWUsT0FBQTtBQUNmLDRHQUFBLGtCQUFrQixPQUFBO0FBQ2xCLDJIQUFBLGlDQUFpQyxPQUFBO0FBQ2pDLHFIQUFBLDJCQUEyQixPQUFBO0FBQzNCLGtIQUFBLHdCQUF3QixPQUFBO0FBQ3hCLG9IQUFBLDBCQUEwQixPQUFBO0FBQzFCLG9IQUFBLDBCQUEwQixPQUFBO0FBQzFCLCtHQUFBLHFCQUFxQixPQUFBO0FBRXZCLHFFQUE2RTtBQUFwRSx1SUFBQSxPQUFPLE9BQW9CO0FBQ3BDLCtFQUF1RjtBQUE5RSxpSkFBQSxPQUFPLE9BQXlCO0FBQ3pDLG1GQUEwRjtBQUFqRixvSkFBQSxPQUFPLE9BQTBCO0FBQzFDLHVGQUE4RjtBQUFyRix3SkFBQSxPQUFPLE9BQTRCO0FBQzVDLHVHQUE2RztBQUFwRyx1S0FBQSxPQUFPLE9BQW1DO0FBQ25ELG1HQUF5RztBQUFoRyxtS0FBQSxPQUFPLE9BQWlDO0FBQ2pELG1FQUEyRTtBQUFsRSxxSUFBQSxPQUFPLE9BQW1CO0FBQ25DLDZFQUFxRjtBQUE1RSwrSUFBQSxPQUFPLE9BQXdCO0FBQ3hDLDhFQUFxRjtBQUE1RSxnSkFBQSxPQUFPLE9BQXdCO0FBQ3hDLHNGQUE2RjtBQUFwRix3SkFBQSxPQUFPLE9BQTRCO0FBQzVDLG9FQUE0RTtBQUFuRSwySUFBQSxPQUFPLE9BQXNCO0FBRXRDLG1DQUE2QztBQUFwQyxpSEFBQSxPQUFPLE9BQVU7QUFFMUIsdURBQWdFO0FBQXZELG9JQUFBLE9BQU8sT0FBbUI7QUFDbkMsK0NBQXdEO0FBQS9DLDRIQUFBLE9BQU8sT0FBZTtBQUMvQixxREFBOEQ7QUFBckQsa0lBQUEsT0FBTyxPQUFrQjtBQUNsQyw0REFBb0U7QUFBM0QsaUlBQUEsT0FBTyxPQUFpQjtBQUNqQywwREFBbUU7QUFBMUQsZ0lBQUEsT0FBTyxPQUFpQjtBQUVqQyxzR0FBNEc7QUFBbkcseUtBQUEsT0FBTyxPQUFvQztBQUVwRCx3R0FBNkc7QUFBcEcsMEtBQUEsT0FBTyxPQUFvQztBQU1wRCx3RUFBaUY7QUFBeEUsOElBQUEsT0FBTyxPQUF3QjtBQUV4Qyw0REFBd0Y7QUFBL0Usd0hBQUEscUJBQXFCLE9BQUE7QUFBRSx3SEFBQSxxQkFBcUIsT0FBQTtBQUNyRCxxRUFBeUY7QUFBaEYsZ0lBQUEscUJBQXFCLE9BQUE7QUFBRSxnSUFBQSxxQkFBcUIsT0FBQTtBQUNyRCx1Q0FBb0M7QUFBM0Isa0dBQUEsTUFBTSxPQUFBO0FBQ2YsMERBQW1FO0FBQTFELDhIQUFBLE9BQU8sT0FBZ0IifQ==
@@ -0,0 +1,5 @@
1
+ export default class KeyedMutex {
2
+ private readonly tails;
3
+ runExclusive<T>(key: string, task: () => Promise<T>): Promise<T>;
4
+ }
5
+ //# sourceMappingURL=keyed-mutex.d.ts.map
@@ -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,2 @@
1
+ export default function assertSafeTokenEndpoint(raw: string): void;
2
+ //# sourceMappingURL=token-endpoint-url.d.ts.map
@@ -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