@codemation/host 0.5.1 → 0.7.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/CHANGELOG.md +465 -0
- package/LICENSE +1 -37
- package/dist/{ApiPaths-CLTHphYZ.js → ApiPaths-Dv1dcHu_.js} +4 -4
- package/dist/ApiPaths-Dv1dcHu_.js.map +1 -0
- package/dist/{AppConfigFactory-CvpFScwB.js → AppConfigFactory-Cx4qQvRk.js} +114 -53
- package/dist/AppConfigFactory-Cx4qQvRk.js.map +1 -0
- package/dist/{AppConfigFactory-LK76niPc.d.ts → AppConfigFactory-DnLoQ9Li.d.ts} +8527 -5549
- package/dist/{AppContainerFactory-BlLrm6_h.js → AppContainerFactory-DqKYCRNP.js} +7656 -2090
- package/dist/AppContainerFactory-DqKYCRNP.js.map +1 -0
- package/dist/{CodemationAppContext-CvWi5gey.d.ts → CodemationAppContext-CKVv9W9q.d.ts} +8 -4
- package/dist/{CodemationAuthoring.types-BuKNTDC1.d.ts → CodemationAuthoring.types-DA3G3s6d.d.ts} +25 -5
- package/dist/{CodemationAuthoring.types-DZl-sJaM.js → CodemationAuthoring.types-NGkBcmmT.js} +18 -6
- package/dist/CodemationAuthoring.types-NGkBcmmT.js.map +1 -0
- package/dist/{CodemationConfigNormalizer-CYdR0PR5.d.ts → CodemationConfigNormalizer-BAKjetJ6.d.ts} +3 -3
- package/dist/{CodemationConsumerConfigLoader-BeAUS144.js → CodemationConsumerConfigLoader-GYpBBvqE.js} +79 -10
- package/dist/CodemationConsumerConfigLoader-GYpBBvqE.js.map +1 -0
- package/dist/{CodemationConsumerConfigLoader-C3nAj9Bj.d.ts → CodemationConsumerConfigLoader-nxOqvv46.d.ts} +17 -2
- package/dist/{CodemationPluginListMerger-B-W5Fa_X.js → CodemationPluginListMerger-D1B1IEbt.js} +1 -1
- package/dist/{CodemationPluginListMerger-B-W5Fa_X.js.map → CodemationPluginListMerger-D1B1IEbt.js.map} +1 -1
- package/dist/{CodemationPluginListMerger-D-gwVwtw.d.ts → CodemationPluginListMerger-DKLAHT2b.d.ts} +123 -16
- package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js +97 -0
- package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js.map +1 -0
- package/dist/{CodemationWhitelabelConfig-CWbcyQqn.d.ts → CodemationWhitelabelConfig-Ca2mCUeC.d.ts} +2 -2
- package/dist/{CollectionContracts.types-DdpHft0i.d.ts → CollectionContracts.types-DDyFYT_D.d.ts} +1 -1
- package/dist/{CredentialContractsRegistry-D7mcPed2.d.ts → CredentialContractsRegistry-Bq2bq28t.d.ts} +2 -2
- package/dist/{CredentialServices-DdCEP2xt.d.ts → CredentialServices-Be2I60Th.d.ts} +65 -20
- package/dist/{CredentialServices-CgxwguAv.js → CredentialServices-Dk8yypeL.js} +310 -51
- package/dist/CredentialServices-Dk8yypeL.js.map +1 -0
- package/dist/InternalHonoApiRouteRegistrar-Ce1yxpnO.d.ts +17 -0
- package/dist/InternalPingRegistrar-DY3kSfxP.js +221 -0
- package/dist/InternalPingRegistrar-DY3kSfxP.js.map +1 -0
- package/dist/{ItemsInputNormalizer-D1WppVMU.d.ts → ItemsInputNormalizer-_RwIfRIQ.d.ts} +108 -25
- package/dist/{LogLevelPolicyFactory-CampWO0l.d.ts → LogLevelPolicyFactory-ewCHLDLn.d.ts} +2 -2
- package/dist/{PublicFrontendBootstrap-DzBgwOnG.d.ts → PublicFrontendBootstrap-Cev3qK46.d.ts} +9 -2
- package/dist/PublicFrontendBootstrapFactory-CY2FS-5g.d.ts +82 -0
- package/dist/{PublicFrontendBootstrapJsonCodec-Cl_DLRh0.d.ts → PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts} +3 -3
- package/dist/{PublicFrontendBootstrapJsonCodec-DzqvA0uo.js → PublicFrontendBootstrapJsonCodec-CegIF_ne.js} +7 -2
- package/dist/PublicFrontendBootstrapJsonCodec-CegIF_ne.js.map +1 -0
- package/dist/ServerLoggerFactory-Ckk52S3w.js +223 -0
- package/dist/ServerLoggerFactory-Ckk52S3w.js.map +1 -0
- package/dist/{TelemetryContracts-BsOD_Y17.d.ts → TelemetryContracts-BtDx84Cp.d.ts} +13 -4
- package/dist/{WorkflowPolicyUiPresentationFactory-DNE5oAI6.d.ts → WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts} +2 -2
- package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js} +1 -1
- package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js.map → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map} +1 -1
- package/dist/{WorkflowViewContracts-0ZgsHQdp.d.ts → WorkflowViewContracts-B7aFQcIw.d.ts} +15 -1
- package/dist/authoring.d.ts +5 -5
- package/dist/authoring.js +1 -1
- package/dist/client.d.ts +4 -4
- package/dist/client.js +2 -2
- package/dist/consumer.d.ts +6 -6
- package/dist/consumer.js +2 -2
- package/dist/credentials.d.ts +6 -6
- package/dist/credentials.js +1 -1
- package/dist/devServerSidecar.d.ts +2 -2
- package/dist/devServerSidecar.js +1 -94
- package/dist/devServerSidecar.js.map +1 -1
- package/dist/dto.d.ts +6 -6
- package/dist/{index-BlGs9e9Q.d.ts → index-DilAYwnH.d.ts} +49 -3
- package/dist/index.d.ts +110 -21
- package/dist/index.js +15 -13
- package/dist/mapping.d.ts +2 -2
- package/dist/mapping.js +1 -1
- package/dist/nextServer.d.ts +43 -88
- package/dist/nextServer.js +9 -7
- package/dist/pairing.d.ts +93 -0
- package/dist/pairing.js +5 -0
- package/dist/pairing.types-snfZ_OzB.d.ts +19 -0
- package/dist/{persistenceServer-CpNFYa_q.js → persistenceServer-C-hH4z6l.js} +2 -2
- package/dist/{persistenceServer-CpNFYa_q.js.map → persistenceServer-C-hH4z6l.js.map} +1 -1
- package/dist/persistenceServer-CeTHtC6E.d.ts +30 -0
- package/dist/persistenceServer.d.ts +8 -8
- package/dist/persistenceServer.js +3 -3
- package/dist/{server-CQWdkT7t.d.ts → server-C4bS62rg.d.ts} +21 -6
- package/dist/{server-BK43OKxW.js → server-Y7kxwtCK.js} +7 -6
- package/dist/{server-BK43OKxW.js.map → server-Y7kxwtCK.js.map} +1 -1
- package/dist/server.d.ts +14 -14
- package/dist/server.js +13 -11
- package/package.json +29 -42
- package/prisma/migrations/20260507120000_execution_instance_child_run_id/migration.sql +5 -0
- package/prisma/migrations/20260519000000_workflow_audit_log/migration.sql +23 -0
- package/prisma/migrations/20260519100000_storage_growth_fixes/migration.sql +61 -0
- package/prisma/migrations.sqlite/20260507120000_execution_instance_child_run_id/migration.sql +5 -0
- package/prisma/migrations.sqlite/20260519000000_workflow_audit_log/migration.sql +21 -0
- package/prisma/migrations.sqlite/20260519100000_storage_growth_fixes/migration.sql +29 -0
- package/prisma/schema.postgresql.prisma +56 -17
- package/prisma/schema.sqlite.prisma +56 -17
- package/prisma-generated/prisma-postgresql-client/edge.js +35 -6
- package/prisma-generated/prisma-postgresql-client/index-browser.js +31 -2
- package/prisma-generated/prisma-postgresql-client/index.d.ts +8971 -5718
- package/prisma-generated/prisma-postgresql-client/index.js +35 -6
- package/prisma-generated/prisma-postgresql-client/package.json +1 -1
- package/prisma-generated/prisma-postgresql-client/schema.prisma +39 -0
- package/prisma-generated/prisma-sqlite-client/edge.js +35 -6
- package/prisma-generated/prisma-sqlite-client/index-browser.js +31 -2
- package/prisma-generated/prisma-sqlite-client/index.d.ts +8963 -5715
- package/prisma-generated/prisma-sqlite-client/index.js +35 -6
- package/prisma-generated/prisma-sqlite-client/package.json +1 -1
- package/prisma-generated/prisma-sqlite-client/schema.prisma +39 -0
- package/scripts/check-collections.mjs +18 -0
- package/scripts/generate-prisma-clients.mjs +20 -11
- package/src/application/WorkflowAuditLogPruneScheduler.ts +96 -0
- package/src/application/auth/AuthenticatedPrincipal.ts +4 -0
- package/src/application/commands/StartWorkflowRunCommandHandler.ts +4 -0
- package/src/application/contracts/WorkflowViewContracts.ts +11 -0
- package/src/application/contracts/WorkflowWebsocketMessage.ts +3 -1
- package/src/application/mapping/WorkflowDefinitionMapper.ts +44 -1
- package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +7 -1
- package/src/application/telemetry/OtelExecutionTelemetry.types.ts +5 -0
- package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +4 -0
- package/src/application/telemetry/StoredTelemetrySpanScope.ts +6 -2
- package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +27 -17
- package/src/application/telemetry/TelemetrySpanPublisher.ts +11 -0
- package/src/application/websocket/TelemetrySpanWebsocketRelay.ts +31 -0
- package/src/applicationTokens.ts +20 -1
- package/src/audit/IAuditEmitter.ts +32 -0
- package/src/audit/PrismaWorkflowAuditLogRepository.ts +34 -0
- package/src/audit/WorkflowAuditLogWriter.ts +125 -0
- package/src/auth/managed/ManagedAuthConfig.ts +29 -0
- package/src/auth/managed/ManagedAuthMiddleware.ts +52 -0
- package/src/auth/managed/ManagedCorsMiddleware.ts +43 -0
- package/src/auth/managed/ManagedModeBootGuard.ts +27 -0
- package/src/auth/managed/index.ts +5 -0
- package/src/bootstrap/AppContainerFactory.ts +277 -29
- package/src/bootstrap/AppContainerLifecycle.ts +31 -0
- package/src/bootstrap/perf/BootTimer.ts +168 -0
- package/src/bootstrap/runtime/AppConfigFactory.ts +21 -65
- package/src/bootstrap/runtime/FrontendRuntime.ts +4 -1
- package/src/bootstrap/runtime/WorkerRuntime.ts +2 -1
- package/src/credentials/BrokerClient.ts +49 -0
- package/src/credentials/BrokerRefreshError.ts +12 -0
- package/src/credentials/BrokerRefreshInvalidGrantError.ts +13 -0
- package/src/credentials/ControlPlaneCatalogFetcher.ts +261 -0
- package/src/credentials/CredentialOAuth2MaterialReader.ts +136 -0
- package/src/credentials/InternalCredentialsListRegistrar.ts +48 -0
- package/src/credentials/InternalCredentialsPushRegistrar.ts +125 -0
- package/src/credentials/LocalOAuthFlowExecutor.ts +316 -0
- package/src/credentials/ManagedOAuthFlowExecutor.ts +94 -0
- package/src/credentials/ManagedOAuthRefreshInvalidGrantError.ts +13 -0
- package/src/credentials/catalogTypes.ts +4 -0
- package/src/credentials/refresh/CredentialDisconnectedError.ts +11 -0
- package/src/domain/credentials/CredentialBindingService.ts +54 -2
- package/src/domain/credentials/CredentialKeyRotatedError.ts +22 -0
- package/src/domain/credentials/CredentialSecretCipher.ts +68 -6
- package/src/domain/credentials/CredentialTypeRegistryImpl.ts +117 -10
- package/src/domain/credentials/OAuth2RedirectUriResolver.ts +79 -0
- package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +14 -5
- package/src/domain/telemetry/TelemetryContracts.ts +7 -1
- package/src/domain/workflows/WorkflowActivationPreflight.ts +24 -1
- package/src/domain/workflows/WorkflowActivationPreflightRules.ts +40 -1
- package/src/index.ts +6 -0
- package/src/infrastructure/binary/LocalFilesystemBinaryStorageRegistry.ts +29 -1
- package/src/infrastructure/binary/S3BinaryStorage.ts +169 -0
- package/src/infrastructure/binary/S3BinaryStorageConfig.ts +17 -0
- package/src/infrastructure/config/CodemationPluginRegistrar.ts +3 -1
- package/src/infrastructure/persistence/CodemationDatabaseUrlParser.ts +41 -0
- package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +8 -3
- package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +1 -0
- package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +21 -13
- package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +43 -8
- package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +33 -3
- package/src/infrastructure/persistence/PrismaWorkflowSnapshotRepository.ts +48 -0
- package/src/mcp/AgentMcpIntegrationImpl.ts +344 -0
- package/src/mcp/McpClientFactory.ts +29 -0
- package/src/mcp/McpConnectionPool.ts +184 -0
- package/src/mcp/McpConnectionPool.types.ts +12 -0
- package/src/mcp/McpServerCatalog.ts +104 -0
- package/src/mcp/index.ts +5 -0
- package/src/pairing/HmacRequestSigner.ts +32 -0
- package/src/pairing/IncomingHmacVerifier.ts +82 -0
- package/src/pairing/InternalHmacAuthMiddleware.ts +33 -0
- package/src/pairing/InternalPingRegistrar.ts +25 -0
- package/src/pairing/PairedFetch.ts +33 -0
- package/src/pairing/PairingConfigFactory.ts +35 -0
- package/src/pairing/PairingConfigToken.ts +6 -0
- package/src/pairing/index.ts +14 -0
- package/src/pairing/pairing.types.ts +18 -0
- package/src/pairing.ts +17 -0
- package/src/persistenceServer.ts +1 -0
- package/src/presentation/config/AppConfig.ts +7 -1
- package/src/presentation/config/CodemationAuthConfig.ts +1 -1
- package/src/presentation/config/CodemationAuthoring.types.ts +54 -5
- package/src/presentation/config/CodemationConfig.ts +3 -0
- package/src/presentation/config/CodemationConfigNormalizer.ts +39 -1
- package/src/presentation/config/CodemationPlugin.ts +2 -1
- package/src/presentation/frontend/CodemationFrontendAuthSnapshot.ts +5 -0
- package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -1
- package/src/presentation/frontend/PublicFrontendBootstrap.ts +2 -0
- package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +5 -1
- package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +4 -1
- package/src/presentation/http/ApiPaths.ts +4 -4
- package/src/presentation/http/ServerHttpErrorResponseFactory.ts +39 -2
- package/src/presentation/http/hono/CodemationHonoApiAppFactory.ts +33 -8
- package/src/presentation/http/hono/InternalHonoApiRouteRegistrar.ts +12 -0
- package/src/presentation/http/hono/registrars/ManagedMeHonoApiRouteRegistrar.ts +35 -0
- package/src/presentation/http/hono/registrars/OAuth2HonoApiRouteRegistrar.ts +2 -2
- package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +28 -0
- package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +98 -41
- package/src/presentation/server/CodemationConsumerConfigLoader.ts +54 -7
- package/src/presentation/server/CodemationPluginDiscovery.ts +5 -0
- package/src/presentation/server/WorkflowDefinitionExportsResolver.ts +18 -0
- package/src/presentation/server/WorkflowModulePathFinder.ts +12 -1
- package/src/presentation/websocket/ManagedWebsocketAuthenticator.ts +50 -0
- package/src/presentation/websocket/WebsocketAuthenticator.types.ts +12 -0
- package/src/presentation/websocket/WorkflowWebsocketServer.ts +24 -3
- package/src/process/ExecaProcessRunner.ts +41 -0
- package/src/process/ProcessRunner.types.ts +39 -0
- package/src/server.ts +2 -0
- package/src/workflows/InternalWorkflowActivationRegistrar.ts +42 -0
- package/src/workflows/InternalWorkflowDetailRegistrar.ts +33 -0
- package/src/workflows/InternalWorkflowTestRunRegistrar.ts +91 -0
- package/src/workflows/InternalWorkflowsListRegistrar.ts +28 -0
- package/src/workflows/discovery/WorkflowDirectoryDiscoverer.ts +79 -0
- package/tsconfig.json +2 -0
- package/vitest.shared.ts +5 -0
- package/dist/ApiPaths-CLTHphYZ.js.map +0 -1
- package/dist/AppConfigFactory-CvpFScwB.js.map +0 -1
- package/dist/AppContainerFactory-BlLrm6_h.js.map +0 -1
- package/dist/CodemationAuthoring.types-DZl-sJaM.js.map +0 -1
- package/dist/CodemationConsumerConfigLoader-BeAUS144.js.map +0 -1
- package/dist/CredentialServices-CgxwguAv.js.map +0 -1
- package/dist/PublicFrontendBootstrapFactory-BMWqNM9a.d.ts +0 -45
- package/dist/PublicFrontendBootstrapJsonCodec-DzqvA0uo.js.map +0 -1
- package/dist/ServerLoggerFactory-BKSIh9Xv.js +0 -98
- package/dist/ServerLoggerFactory-BKSIh9Xv.js.map +0 -1
- package/dist/persistenceServer-CIVt3UOX.d.ts +0 -9
- package/src/domain/credentials/OAuth2ConnectServiceFactory.ts +0 -411
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import { ApplicationTokens } from "../applicationTokens";
|
|
3
|
+
import type { LoggerFactory } from "../application/logging/Logger";
|
|
4
|
+
import { McpServerCatalog } from "./McpServerCatalog";
|
|
5
|
+
import { DefaultMcpClientFactory } from "./McpClientFactory";
|
|
6
|
+
import type { McpClientFactory } from "./McpClientFactory";
|
|
7
|
+
import { CredentialOAuth2MaterialReader } from "../credentials/CredentialOAuth2MaterialReader";
|
|
8
|
+
import type { MCPClient, McpToolSet } from "./McpConnectionPool.types";
|
|
9
|
+
|
|
10
|
+
/** Mutable internal pool entry (toolsCache may be filled lazily). */
|
|
11
|
+
type MutablePoolEntry = {
|
|
12
|
+
client: MCPClient;
|
|
13
|
+
toolsCache: McpToolSet | null;
|
|
14
|
+
openedAt: Date;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
@injectable()
|
|
18
|
+
export class McpConnectionPool {
|
|
19
|
+
/** Key: `${credentialInstanceId}:${serverId}` */
|
|
20
|
+
private readonly pool = new Map<string, MutablePoolEntry>();
|
|
21
|
+
/**
|
|
22
|
+
* In-flight open promises — prevents a double-open race when two callers request
|
|
23
|
+
* the same (credentialInstanceId, serverId) pair concurrently.
|
|
24
|
+
*/
|
|
25
|
+
private readonly inFlight = new Map<string, Promise<MutablePoolEntry>>();
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
@inject(McpServerCatalog) private readonly catalog: McpServerCatalog,
|
|
29
|
+
@inject(CredentialOAuth2MaterialReader) private readonly oauth2Material: CredentialOAuth2MaterialReader,
|
|
30
|
+
@inject(ApplicationTokens.LoggerFactory) private readonly loggers: LoggerFactory,
|
|
31
|
+
@inject(DefaultMcpClientFactory) private readonly clientFactory: McpClientFactory,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns a live MCP client for the given credential instance + server.
|
|
36
|
+
* Opens a new connection lazily; subsequent calls with the same pair return the cached client.
|
|
37
|
+
* Two concurrent calls for the same pair share a single open operation (single-flight).
|
|
38
|
+
*/
|
|
39
|
+
async getClient(credentialInstanceId: string, serverId: string): Promise<MCPClient> {
|
|
40
|
+
const entry = await this.getOrOpenEntry(credentialInstanceId, serverId);
|
|
41
|
+
return entry.client;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the tools/list result for the given credential instance + server,
|
|
46
|
+
* with toolDescriptionOverrides from the declaration applied.
|
|
47
|
+
* Fetches and caches once per pool entry; subsequent calls return the cached value.
|
|
48
|
+
* Used by Story 10's BM25 indexer.
|
|
49
|
+
*/
|
|
50
|
+
async getTools(credentialInstanceId: string, serverId: string): Promise<McpToolSet> {
|
|
51
|
+
const entry = await this.getOrOpenEntry(credentialInstanceId, serverId);
|
|
52
|
+
if (!entry.toolsCache) {
|
|
53
|
+
const raw = await entry.client.tools();
|
|
54
|
+
const decl = this.catalog.get(serverId);
|
|
55
|
+
entry.toolsCache = this.applyOverrides(raw, decl?.toolDescriptionOverrides);
|
|
56
|
+
}
|
|
57
|
+
return entry.toolsCache;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Closes all pool entries for a credential instance.
|
|
62
|
+
* Call this when the credential is revoked or disconnected.
|
|
63
|
+
* Token refresh does NOT require closing — OAuthFlowExecutor
|
|
64
|
+
* keeps the stored token fresh; the next open will read the current token.
|
|
65
|
+
*
|
|
66
|
+
* Resolves after all matched clients have completed close(), so callers can
|
|
67
|
+
* await this before re-connecting or cleaning up downstream state.
|
|
68
|
+
*
|
|
69
|
+
* TODO(story-credential-lifecycle): Wire this method to the credential lifecycle event.
|
|
70
|
+
* CredentialDisconnectedError (packages/host/src/credentials/refresh/CredentialDisconnectedError.ts)
|
|
71
|
+
* is thrown on dead refresh tokens but is an error, not a broadcast event — there is no
|
|
72
|
+
* event bus for credential lifecycle today. When a credential-disconnected event mechanism
|
|
73
|
+
* is introduced, call closeForCredential(credentialInstanceId) from its handler so that
|
|
74
|
+
* stale MCP pool entries are cleaned up on credential revocation.
|
|
75
|
+
*/
|
|
76
|
+
async closeForCredential(credentialInstanceId: string): Promise<void> {
|
|
77
|
+
const logger = this.loggers.create("McpConnectionPool");
|
|
78
|
+
const prefix = `${credentialInstanceId}:`;
|
|
79
|
+
const toClose: Array<[string, MutablePoolEntry]> = [];
|
|
80
|
+
for (const [key, entry] of this.pool.entries()) {
|
|
81
|
+
if (key.startsWith(prefix)) {
|
|
82
|
+
toClose.push([key, entry]);
|
|
83
|
+
this.pool.delete(key);
|
|
84
|
+
logger.info(`McpConnectionPool: closed pool entry on credential revocation (key=${key})`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await Promise.allSettled(
|
|
88
|
+
toClose.map(([key, entry]) =>
|
|
89
|
+
entry.client.close().catch((e: unknown) => {
|
|
90
|
+
logger.warn(
|
|
91
|
+
`McpConnectionPool: error closing client on credential revocation (key=${key})`,
|
|
92
|
+
e instanceof Error ? e : undefined,
|
|
93
|
+
);
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Closes all pool entries. Called on host shutdown.
|
|
101
|
+
*/
|
|
102
|
+
async closeAll(): Promise<void> {
|
|
103
|
+
await Promise.allSettled([...this.pool.values()].map((e) => e.client.close()));
|
|
104
|
+
this.pool.clear();
|
|
105
|
+
this.inFlight.clear();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async getOrOpenEntry(credentialInstanceId: string, serverId: string): Promise<MutablePoolEntry> {
|
|
109
|
+
const key = this.poolKey(credentialInstanceId, serverId);
|
|
110
|
+
const cached = this.pool.get(key);
|
|
111
|
+
if (cached) {
|
|
112
|
+
return cached;
|
|
113
|
+
}
|
|
114
|
+
const existing = this.inFlight.get(key);
|
|
115
|
+
if (existing) {
|
|
116
|
+
return existing;
|
|
117
|
+
}
|
|
118
|
+
const openPromise = this.open(credentialInstanceId, serverId, key).finally(() => {
|
|
119
|
+
this.inFlight.delete(key);
|
|
120
|
+
});
|
|
121
|
+
this.inFlight.set(key, openPromise);
|
|
122
|
+
return openPromise;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async open(credentialInstanceId: string, serverId: string, key: string): Promise<MutablePoolEntry> {
|
|
126
|
+
const decl = this.catalog.get(serverId);
|
|
127
|
+
if (!decl) {
|
|
128
|
+
throw new Error(`McpConnectionPool: MCP server "${serverId}" not found in catalog`);
|
|
129
|
+
}
|
|
130
|
+
// D1: HTTP-only in managed mode. The catalog already blocks stdio at merge time, but we
|
|
131
|
+
// double-check here as a defensive guard in case a declaration bypasses the catalog (e.g.
|
|
132
|
+
// a future in-memory test harness or a dynamically injected server that skips catalog
|
|
133
|
+
// validation). Transport is an attribute of the declaration, not the env var.
|
|
134
|
+
if (decl.transport !== "http") {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`McpConnectionPool: MCP server "${serverId}" uses transport "${decl.transport}" which is not allowed in managed mode. ` +
|
|
137
|
+
`Only "http" transport is supported. For stdio, set CODEMATION_ALLOW_STDIO_MCP=true in a self-hosted environment.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Read OAuth material directly. The bearer is baked into the client's headers at open
|
|
142
|
+
// time (per-open, not per-call) — @ai-sdk/mcp v1.0.42 does not support per-request header
|
|
143
|
+
// injection. LIMITATION: a pool entry uses a stale bearer after the access token expires;
|
|
144
|
+
// OAuthFlowExecutor refreshes stored material in the background, but the entry must be
|
|
145
|
+
// closed via closeForCredential and re-opened for the refreshed token to take effect.
|
|
146
|
+
const accessToken = await this.readAccessToken(credentialInstanceId, serverId);
|
|
147
|
+
const headers: Record<string, string> = {
|
|
148
|
+
...(decl.staticHeaders ?? {}),
|
|
149
|
+
authorization: `Bearer ${accessToken}`,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const client = await this.clientFactory.open({ url: decl.url, headers });
|
|
153
|
+
const entry: MutablePoolEntry = { client, toolsCache: null, openedAt: new Date() };
|
|
154
|
+
this.pool.set(key, entry);
|
|
155
|
+
return entry;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private poolKey(credentialInstanceId: string, serverId: string): string {
|
|
159
|
+
return `${credentialInstanceId}:${serverId}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async readAccessToken(credentialInstanceId: string, serverId: string): Promise<string> {
|
|
163
|
+
const material = await this.oauth2Material.readMaterial(credentialInstanceId);
|
|
164
|
+
if (!material.accessToken) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`McpConnectionPool: credential instance "${credentialInstanceId}" has no access token — reconnect the credential bound to MCP server "${serverId}"`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return material.accessToken;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private applyOverrides(tools: McpToolSet, overrides?: Record<string, string>): McpToolSet {
|
|
173
|
+
if (!overrides) {
|
|
174
|
+
return tools;
|
|
175
|
+
}
|
|
176
|
+
const result = { ...tools };
|
|
177
|
+
for (const [name, description] of Object.entries(overrides)) {
|
|
178
|
+
if (result[name]) {
|
|
179
|
+
result[name] = { ...result[name], description };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MCPClient } from "@ai-sdk/mcp";
|
|
2
|
+
|
|
3
|
+
export type { MCPClient };
|
|
4
|
+
|
|
5
|
+
/** The ToolSet shape returned by MCPClient.tools() with 'automatic' schema resolution. */
|
|
6
|
+
export type McpToolSet = Awaited<ReturnType<MCPClient["tools"]>>;
|
|
7
|
+
|
|
8
|
+
export type McpPoolEntry = Readonly<{
|
|
9
|
+
client: MCPClient;
|
|
10
|
+
toolsCache: McpToolSet | null;
|
|
11
|
+
openedAt: Date;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import type { McpServerDeclaration } from "@codemation/core";
|
|
3
|
+
import { ApplicationTokens } from "../applicationTokens";
|
|
4
|
+
import type { LoggerFactory } from "../application/logging/Logger";
|
|
5
|
+
import type { AppConfig } from "../presentation/config/AppConfig";
|
|
6
|
+
|
|
7
|
+
export type McpServerDeclarationSource = "plugin" | "config" | "controlPlane";
|
|
8
|
+
|
|
9
|
+
const SOURCE_PRIORITY: Record<McpServerDeclarationSource, number> = {
|
|
10
|
+
plugin: 0,
|
|
11
|
+
config: 1,
|
|
12
|
+
controlPlane: 2,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const ID_PATTERN = /^[a-z0-9-]+$/;
|
|
16
|
+
|
|
17
|
+
type CatalogEntry = Readonly<{
|
|
18
|
+
decl: McpServerDeclaration;
|
|
19
|
+
source: McpServerDeclarationSource;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
@injectable()
|
|
23
|
+
export class McpServerCatalog {
|
|
24
|
+
private readonly entries = new Map<string, CatalogEntry>();
|
|
25
|
+
private readonly bySource = new Map<McpServerDeclarationSource, Set<string>>();
|
|
26
|
+
private readonly env: NodeJS.ProcessEnv;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
@inject(ApplicationTokens.LoggerFactory) private readonly loggers: LoggerFactory,
|
|
30
|
+
@inject(ApplicationTokens.AppConfig) appConfig: AppConfig,
|
|
31
|
+
) {
|
|
32
|
+
this.env = appConfig.env;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
merge(source: McpServerDeclarationSource, declarations: ReadonlyArray<McpServerDeclaration>): void {
|
|
36
|
+
const logger = this.loggers.create("McpServerCatalog");
|
|
37
|
+
for (const decl of declarations) {
|
|
38
|
+
if (!this.validate(decl, source, logger)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const existing = this.entries.get(decl.id);
|
|
42
|
+
if (existing) {
|
|
43
|
+
if (SOURCE_PRIORITY[source] <= SOURCE_PRIORITY[existing.source]) {
|
|
44
|
+
logger.warn(
|
|
45
|
+
`McpServerCatalog: id collision — lower-priority source "${source}" ignored for id "${decl.id}" (current source: "${existing.source}")`,
|
|
46
|
+
);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
logger.warn(
|
|
50
|
+
`McpServerCatalog: id "${decl.id}" shadowed — "${existing.source}" overridden by higher-priority source "${source}"`,
|
|
51
|
+
);
|
|
52
|
+
this.bySource.get(existing.source)?.delete(decl.id);
|
|
53
|
+
}
|
|
54
|
+
this.entries.set(decl.id, { decl, source });
|
|
55
|
+
if (!this.bySource.has(source)) {
|
|
56
|
+
this.bySource.set(source, new Set());
|
|
57
|
+
}
|
|
58
|
+
this.bySource.get(source)!.add(decl.id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get(id: string): McpServerDeclaration | undefined {
|
|
63
|
+
return this.entries.get(id)?.decl;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getAll(): readonly McpServerDeclaration[] {
|
|
67
|
+
return [...this.entries.values()].map((entry) => entry.decl);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
clear(source: McpServerDeclarationSource): void {
|
|
71
|
+
const ids = this.bySource.get(source);
|
|
72
|
+
if (!ids) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const id of ids) {
|
|
76
|
+
this.entries.delete(id);
|
|
77
|
+
}
|
|
78
|
+
this.bySource.delete(source);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private validate(
|
|
82
|
+
decl: McpServerDeclaration,
|
|
83
|
+
source: McpServerDeclarationSource,
|
|
84
|
+
logger: ReturnType<LoggerFactory["create"]>,
|
|
85
|
+
): boolean {
|
|
86
|
+
if (!ID_PATTERN.test(decl.id)) {
|
|
87
|
+
logger.warn(
|
|
88
|
+
`McpServerCatalog: declaration from "${source}" has invalid id "${decl.id}" (must match /^[a-z0-9-]+$/) — skipped`,
|
|
89
|
+
);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if ((decl.transport as string) === "stdio") {
|
|
94
|
+
if (this.env.CODEMATION_ALLOW_STDIO_MCP !== "true") {
|
|
95
|
+
logger.warn(
|
|
96
|
+
`McpServerCatalog: declaration "${decl.id}" from "${source}" uses stdio transport which is disabled (set CODEMATION_ALLOW_STDIO_MCP=true to allow) — skipped`,
|
|
97
|
+
);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { McpServerCatalog, type McpServerDeclarationSource } from "./McpServerCatalog";
|
|
2
|
+
export { McpConnectionPool } from "./McpConnectionPool";
|
|
3
|
+
export type { McpPoolEntry, McpToolSet, MCPClient } from "./McpConnectionPool.types";
|
|
4
|
+
export { DefaultMcpClientFactory, type McpClientFactory, type McpClientOpenArgs } from "./McpClientFactory";
|
|
5
|
+
export { AgentMcpIntegrationImpl } from "./AgentMcpIntegrationImpl";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHmac, createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { inject, injectable } from "@codemation/core";
|
|
3
|
+
import type { PairingConfig } from "./pairing.types";
|
|
4
|
+
import { PairingConfigToken } from "./PairingConfigToken";
|
|
5
|
+
|
|
6
|
+
export interface SignedHeaders {
|
|
7
|
+
readonly Authorization: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@injectable()
|
|
11
|
+
export class HmacRequestSigner {
|
|
12
|
+
constructor(@inject(PairingConfigToken) private readonly config: PairingConfig) {}
|
|
13
|
+
|
|
14
|
+
sign(method: string, urlOrPath: string, body: string): SignedHeaders {
|
|
15
|
+
const ts = Math.floor(Date.now() / 1000);
|
|
16
|
+
const nonce = randomBytes(16).toString("base64");
|
|
17
|
+
|
|
18
|
+
const parsed = new URL(urlOrPath, "http://placeholder");
|
|
19
|
+
const path = (parsed.pathname + parsed.search).toLowerCase();
|
|
20
|
+
|
|
21
|
+
const bodyHash = createHash("sha256").update(body, "utf8").digest("hex");
|
|
22
|
+
const baseString = [method.toUpperCase(), path, ts, nonce, bodyHash].join("\n");
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line codemation/no-buffer-everything -- pairing secret is 32 bytes, never large
|
|
25
|
+
const secretBytes = Buffer.from(this.config.pairingSecret, "base64");
|
|
26
|
+
const sig = createHmac("sha256", secretBytes).update(baseString, "utf8").digest("base64");
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
Authorization: `Codemation-Hmac v=1,workspaceId=${this.config.workspaceId},ts=${ts},nonce=${nonce},sig=${sig}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHmac, createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { inject, injectable } from "@codemation/core";
|
|
3
|
+
import type { PairingConfig, PairingVerificationResult } from "./pairing.types";
|
|
4
|
+
import { PairingConfigToken } from "./PairingConfigToken";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Verifies incoming HMAC-signed requests from the control plane.
|
|
8
|
+
* Mirrors the control-plane HmacVerifier — both sides follow docs/pairing-protocol.md.
|
|
9
|
+
*/
|
|
10
|
+
@injectable()
|
|
11
|
+
export class IncomingHmacVerifier {
|
|
12
|
+
private readonly usedNonces = new Map<string, number>();
|
|
13
|
+
private readonly nonceTtlSeconds = 600; // 10 minutes
|
|
14
|
+
|
|
15
|
+
constructor(@inject(PairingConfigToken) private readonly config: PairingConfig) {}
|
|
16
|
+
|
|
17
|
+
verify(method: string, url: string, body: string, authHeader: string | null): PairingVerificationResult {
|
|
18
|
+
if (!this.config.pairingSecret || this.config.pairingSecret.trim().length === 0) {
|
|
19
|
+
throw new Error("IncomingHmacVerifier: pairingSecret is not configured — cannot verify HMAC requests.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!authHeader?.startsWith("Codemation-Hmac ")) {
|
|
23
|
+
return { failure: "missing" };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parts = this.parseHeader(authHeader);
|
|
27
|
+
if (!parts) return { failure: "missing" };
|
|
28
|
+
if (parts.v !== "1") return { failure: "version" };
|
|
29
|
+
|
|
30
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
31
|
+
if (Math.abs(nowSec - parts.ts) > 300) return { failure: "expired" };
|
|
32
|
+
|
|
33
|
+
if (parts.workspaceId !== this.config.workspaceId) return { failure: "workspace" };
|
|
34
|
+
|
|
35
|
+
const parsed = new URL(url, "http://placeholder");
|
|
36
|
+
const path = (parsed.pathname + parsed.search).toLowerCase();
|
|
37
|
+
const bodyHash = createHash("sha256").update(body, "utf8").digest("hex");
|
|
38
|
+
const baseString = [method.toUpperCase(), path, parts.ts, parts.nonce, bodyHash].join("\n");
|
|
39
|
+
|
|
40
|
+
// eslint-disable-next-line codemation/no-buffer-everything -- pairing secret is 32 bytes, never large
|
|
41
|
+
const secretBytes = Buffer.from(this.config.pairingSecret, "base64");
|
|
42
|
+
const expected = createHmac("sha256", secretBytes).update(baseString, "utf8").digest("base64");
|
|
43
|
+
|
|
44
|
+
const expectedBuf = Buffer.from(expected);
|
|
45
|
+
const actualBuf = Buffer.from(parts.sig);
|
|
46
|
+
if (expectedBuf.length !== actualBuf.length || !timingSafeEqual(expectedBuf, actualBuf)) {
|
|
47
|
+
return { failure: "signature" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.pruneExpiredNonces(nowSec);
|
|
51
|
+
const nonceKey = `${parts.workspaceId}:${parts.nonce}`;
|
|
52
|
+
if (this.usedNonces.has(nonceKey)) return { failure: "replay" };
|
|
53
|
+
this.usedNonces.set(nonceKey, nowSec + this.nonceTtlSeconds);
|
|
54
|
+
|
|
55
|
+
return { workspaceId: parts.workspaceId };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private parseHeader(header: string): {
|
|
59
|
+
v: string;
|
|
60
|
+
workspaceId: string;
|
|
61
|
+
ts: number;
|
|
62
|
+
nonce: string;
|
|
63
|
+
sig: string;
|
|
64
|
+
} | null {
|
|
65
|
+
const payload = header.slice("Codemation-Hmac ".length);
|
|
66
|
+
const fields: Record<string, string> = {};
|
|
67
|
+
for (const part of payload.split(",")) {
|
|
68
|
+
const eq = part.indexOf("=");
|
|
69
|
+
if (eq === -1) return null;
|
|
70
|
+
fields[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
|
|
71
|
+
}
|
|
72
|
+
const { v, workspaceId, ts, nonce, sig } = fields;
|
|
73
|
+
if (!v || !workspaceId || !ts || !nonce || !sig) return null;
|
|
74
|
+
return { v, workspaceId, ts: Number(ts), nonce, sig };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private pruneExpiredNonces(nowSec: number): void {
|
|
78
|
+
for (const [key, expiry] of this.usedNonces.entries()) {
|
|
79
|
+
if (expiry <= nowSec) this.usedNonces.delete(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import type { Context, MiddlewareHandler, Next } from "hono";
|
|
3
|
+
import { IncomingHmacVerifier } from "./IncomingHmacVerifier";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hono middleware that verifies HMAC-signed requests on /internal/* routes.
|
|
7
|
+
* Rejects with 401 on any auth failure (failure mode is never leaked).
|
|
8
|
+
*
|
|
9
|
+
* Downstream handlers read the consumed body from `c.get("body")` when needed —
|
|
10
|
+
* do NOT call `c.req.text()` again after this middleware runs.
|
|
11
|
+
*/
|
|
12
|
+
@injectable()
|
|
13
|
+
export class InternalHmacAuthMiddleware {
|
|
14
|
+
constructor(@inject(IncomingHmacVerifier) private readonly verifier: IncomingHmacVerifier) {}
|
|
15
|
+
|
|
16
|
+
handle(): MiddlewareHandler {
|
|
17
|
+
return async (c: Context, next: Next) => {
|
|
18
|
+
const body = c.req.method === "GET" || c.req.method === "HEAD" ? "" : await c.req.text();
|
|
19
|
+
|
|
20
|
+
const result = this.verifier.verify(c.req.method, c.req.url, body, c.req.header("authorization") ?? null);
|
|
21
|
+
|
|
22
|
+
if ("failure" in result) {
|
|
23
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Body stored for downstream handlers that need it (e.g., credential push).
|
|
27
|
+
// Access via c.get("body") — do NOT call c.req.text() again.
|
|
28
|
+
// workspaceId is available from PairingConfig since installation has a single workspace.
|
|
29
|
+
c.set("body" as never, body);
|
|
30
|
+
await next();
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
import { InternalHmacAuthMiddleware } from "./InternalHmacAuthMiddleware";
|
|
4
|
+
import type { InternalHonoApiRouteRegistrar } from "../presentation/http/hono/InternalHonoApiRouteRegistrar";
|
|
5
|
+
import type { PairingConfig } from "./pairing.types";
|
|
6
|
+
import { PairingConfigToken } from "./PairingConfigToken";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Registers GET /internal/ping — a smoke-test endpoint for verifying workspace pairing.
|
|
10
|
+
* Returns { pong: true, workspaceId } when the HMAC signature validates correctly.
|
|
11
|
+
*/
|
|
12
|
+
@injectable()
|
|
13
|
+
export class InternalPingRegistrar implements InternalHonoApiRouteRegistrar {
|
|
14
|
+
constructor(
|
|
15
|
+
@inject(InternalHmacAuthMiddleware) private readonly hmacMiddleware: InternalHmacAuthMiddleware,
|
|
16
|
+
@inject(PairingConfigToken) private readonly pairingConfig: PairingConfig,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
register(app: Hono): void {
|
|
20
|
+
const { workspaceId } = this.pairingConfig;
|
|
21
|
+
app.get("/internal/ping", this.hmacMiddleware.handle(), (c) => {
|
|
22
|
+
return c.json({ pong: true, workspaceId });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import { HmacRequestSigner } from "./HmacRequestSigner";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Thin fetch wrapper that automatically HMAC-signs outgoing requests
|
|
6
|
+
* to the control plane using the workspace's pairing secret.
|
|
7
|
+
*
|
|
8
|
+
* Use this for any server-to-server request from the installation to the CP.
|
|
9
|
+
*/
|
|
10
|
+
@injectable()
|
|
11
|
+
export class PairedFetch {
|
|
12
|
+
constructor(@inject(HmacRequestSigner) private readonly signer: HmacRequestSigner) {}
|
|
13
|
+
|
|
14
|
+
async get(url: string): Promise<Response> {
|
|
15
|
+
const headers = this.signer.sign("GET", url, "");
|
|
16
|
+
return fetch(url, { method: "GET", headers: { ...headers } });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async post(url: string, body: unknown): Promise<Response> {
|
|
20
|
+
const bodyString = JSON.stringify(body);
|
|
21
|
+
const headers = this.signer.sign("POST", url, bodyString);
|
|
22
|
+
return fetch(url, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
25
|
+
body: bodyString,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async delete(url: string): Promise<Response> {
|
|
30
|
+
const headers = this.signer.sign("DELETE", url, "");
|
|
31
|
+
return fetch(url, { method: "DELETE", headers: { ...headers } });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PairingConfig } from "./pairing.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reads pairing configuration from environment variables.
|
|
5
|
+
*
|
|
6
|
+
* Required env vars when pairing is enabled:
|
|
7
|
+
* WORKSPACE_ID — the workspace's database ID
|
|
8
|
+
* WORKSPACE_PAIRING_SECRET — base64-encoded 32-byte shared secret
|
|
9
|
+
* CONTROL_PLANE_URL — base URL of the control plane API
|
|
10
|
+
*
|
|
11
|
+
* Returns null if any required variable is absent (pairing disabled).
|
|
12
|
+
* See docs/pairing-protocol.md for full bootstrap instructions.
|
|
13
|
+
*/
|
|
14
|
+
export class PairingConfigFactory {
|
|
15
|
+
create(env: Readonly<NodeJS.ProcessEnv>): PairingConfig | null {
|
|
16
|
+
const workspaceId = env["WORKSPACE_ID"];
|
|
17
|
+
const pairingSecret = env["WORKSPACE_PAIRING_SECRET"];
|
|
18
|
+
const controlPlaneUrl = env["CONTROL_PLANE_URL"];
|
|
19
|
+
|
|
20
|
+
if (!workspaceId || !pairingSecret || !controlPlaneUrl) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line codemation/no-buffer-everything -- pairing secret is always 32 bytes; bounded by validation below.
|
|
25
|
+
const decoded = Buffer.from(pairingSecret, "base64");
|
|
26
|
+
if (decoded.length !== 32) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`WORKSPACE_PAIRING_SECRET must be a base64-encoded 32-byte value (got ${decoded.length} bytes). ` +
|
|
29
|
+
`Generate a valid secret with: openssl rand -base64 32`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { workspaceId, pairingSecret, controlPlaneUrl };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TypeToken } from "@codemation/core";
|
|
2
|
+
import type { PairingConfig } from "./pairing.types";
|
|
3
|
+
|
|
4
|
+
// Symbol token so the DI container can inject PairingConfig.
|
|
5
|
+
// Registered by PairingConfigFactory in the composition root.
|
|
6
|
+
export const PairingConfigToken = Symbol.for("codemation.pairing.PairingConfig") as TypeToken<PairingConfig>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { HmacRequestSigner } from "./HmacRequestSigner";
|
|
2
|
+
export type { SignedHeaders } from "./HmacRequestSigner";
|
|
3
|
+
export { PairedFetch } from "./PairedFetch";
|
|
4
|
+
export { IncomingHmacVerifier } from "./IncomingHmacVerifier";
|
|
5
|
+
export { InternalHmacAuthMiddleware } from "./InternalHmacAuthMiddleware";
|
|
6
|
+
export { InternalPingRegistrar } from "./InternalPingRegistrar";
|
|
7
|
+
export { PairingConfigFactory } from "./PairingConfigFactory";
|
|
8
|
+
export { PairingConfigToken } from "./PairingConfigToken";
|
|
9
|
+
export type {
|
|
10
|
+
PairingConfig,
|
|
11
|
+
PairingVerificationResult,
|
|
12
|
+
PairingVerificationFailure,
|
|
13
|
+
PairingVerificationSuccess,
|
|
14
|
+
} from "./pairing.types";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PairingConfig {
|
|
2
|
+
/** The workspace's database ID. */
|
|
3
|
+
readonly workspaceId: string;
|
|
4
|
+
/** Base64-encoded 32-byte raw secret shared with the control plane. */
|
|
5
|
+
readonly pairingSecret: string;
|
|
6
|
+
/** Base URL of the control plane API, e.g. https://api.codemation.io */
|
|
7
|
+
readonly controlPlaneUrl: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type PairingVerificationFailure = {
|
|
11
|
+
readonly failure: "missing" | "version" | "expired" | "workspace" | "signature" | "replay";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type PairingVerificationSuccess = {
|
|
15
|
+
readonly workspaceId: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type PairingVerificationResult = PairingVerificationSuccess | PairingVerificationFailure;
|
package/src/pairing.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Subpath entry point: @codemation/host/pairing
|
|
2
|
+
export {
|
|
3
|
+
HmacRequestSigner,
|
|
4
|
+
PairedFetch,
|
|
5
|
+
IncomingHmacVerifier,
|
|
6
|
+
InternalHmacAuthMiddleware,
|
|
7
|
+
InternalPingRegistrar,
|
|
8
|
+
PairingConfigFactory,
|
|
9
|
+
PairingConfigToken,
|
|
10
|
+
} from "./pairing/index";
|
|
11
|
+
export type {
|
|
12
|
+
PairingConfig,
|
|
13
|
+
PairingVerificationResult,
|
|
14
|
+
PairingVerificationFailure,
|
|
15
|
+
PairingVerificationSuccess,
|
|
16
|
+
SignedHeaders,
|
|
17
|
+
} from "./pairing/index";
|
package/src/persistenceServer.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { CodemationPostgresPrismaClientFactory } from "./infrastructure/persiste
|
|
|
2
2
|
export type { AppPersistenceConfig } from "./presentation/config/AppConfig";
|
|
3
3
|
export { AppConfigFactory } from "./bootstrap/runtime/AppConfigFactory";
|
|
4
4
|
export { PrismaMigrationDeployer } from "./infrastructure/persistence/PrismaMigrationDeployer";
|
|
5
|
+
export { CodemationDatabaseUrlParser } from "./infrastructure/persistence/CodemationDatabaseUrlParser";
|
|
5
6
|
export type { PrismaDatabaseClient as PrismaClient } from "./infrastructure/persistence/PrismaDatabaseClient";
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AnyCredentialType,
|
|
3
|
+
CollectionDefinition,
|
|
4
|
+
McpServerDeclaration,
|
|
5
|
+
WorkflowDefinition,
|
|
6
|
+
} from "@codemation/core";
|
|
2
7
|
import type { CodemationContainerRegistration } from "../../bootstrap/CodemationContainerRegistration";
|
|
3
8
|
import type { CodemationPlugin } from "./CodemationPlugin";
|
|
4
9
|
import type {
|
|
@@ -32,6 +37,7 @@ export interface AppConfig {
|
|
|
32
37
|
readonly collections: ReadonlyArray<CollectionDefinition>;
|
|
33
38
|
readonly plugins: ReadonlyArray<CodemationPlugin>;
|
|
34
39
|
readonly pluginLoadSummary?: ReadonlyArray<AppPluginLoadSummary>;
|
|
40
|
+
readonly mcpServers: ReadonlyArray<McpServerDeclaration>;
|
|
35
41
|
readonly hasConfiguredCredentialSessionServiceRegistration: boolean;
|
|
36
42
|
readonly log?: CodemationLogConfig;
|
|
37
43
|
readonly engineExecutionLimits?: CodemationEngineExecutionLimitsConfig;
|
|
@@ -4,7 +4,7 @@ import type { BetterAuthOptions } from "better-auth";
|
|
|
4
4
|
* Consumer-declared authentication profile for the hosted UI + HTTP API.
|
|
5
5
|
* Social provider ids intentionally match Better Auth's provider ids so config stays 1:1 with the auth runtime.
|
|
6
6
|
*/
|
|
7
|
-
export type CodemationAuthKind = "local" | "oauth" | "oidc";
|
|
7
|
+
export type CodemationAuthKind = "local" | "oauth" | "oidc" | "managed";
|
|
8
8
|
|
|
9
9
|
export type CodemationAuthOAuthProviderId = Extract<
|
|
10
10
|
keyof NonNullable<BetterAuthOptions["socialProviders"]>,
|