@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
|
@@ -1,79 +1,136 @@
|
|
|
1
|
+
import type { OAuthFlowExecutor } from "@codemation/core";
|
|
1
2
|
import { inject, injectable } from "@codemation/core";
|
|
2
3
|
import serialize from "serialize-javascript";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { ApplicationTokens } from "../../../applicationTokens";
|
|
5
|
+
import {
|
|
6
|
+
CredentialInstanceService,
|
|
7
|
+
CredentialSecretCipher,
|
|
8
|
+
type CredentialStore,
|
|
9
|
+
} from "../../../domain/credentials/CredentialServices";
|
|
10
|
+
import { OAuth2RedirectUriResolver } from "../../../domain/credentials/OAuth2RedirectUriResolver";
|
|
11
|
+
import { HttpRequestJsonBodyReader } from "../HttpRequestJsonBodyReader";
|
|
5
12
|
import { ServerHttpErrorResponseFactory } from "../ServerHttpErrorResponseFactory";
|
|
6
13
|
|
|
14
|
+
type OAuthStartRequestBody = Readonly<{
|
|
15
|
+
typeId: string;
|
|
16
|
+
instanceId: string;
|
|
17
|
+
redirectUri: string;
|
|
18
|
+
scopes?: ReadonlyArray<string>;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
7
21
|
@injectable()
|
|
8
22
|
export class OAuth2HttpRouteHandler {
|
|
9
23
|
constructor(
|
|
10
|
-
@inject(
|
|
11
|
-
private readonly
|
|
24
|
+
@inject(OAuth2RedirectUriResolver)
|
|
25
|
+
private readonly redirectUriResolver: OAuth2RedirectUriResolver,
|
|
12
26
|
@inject(CredentialInstanceService)
|
|
13
27
|
private readonly credentialInstanceService: CredentialInstanceService,
|
|
28
|
+
@inject(ApplicationTokens.OAuthFlowExecutor)
|
|
29
|
+
private readonly oauthFlowExecutor: OAuthFlowExecutor,
|
|
30
|
+
@inject(ApplicationTokens.CredentialStore)
|
|
31
|
+
private readonly credentialStore: CredentialStore,
|
|
32
|
+
@inject(CredentialSecretCipher)
|
|
33
|
+
private readonly credentialSecretCipher: CredentialSecretCipher,
|
|
14
34
|
) {}
|
|
15
35
|
|
|
16
|
-
async
|
|
36
|
+
async getRedirectUri(request: Request): Promise<Response> {
|
|
17
37
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return Response.json({ error: "Missing instanceId query parameter." }, { status: 400 });
|
|
22
|
-
}
|
|
23
|
-
const redirect = await this.oauth2ConnectService.createAuthRedirect(
|
|
24
|
-
instanceId,
|
|
25
|
-
this.resolveRequestOrigin(request),
|
|
26
|
-
);
|
|
27
|
-
return Response.redirect(redirect.redirectUrl, 302);
|
|
38
|
+
return Response.json({
|
|
39
|
+
redirectUri: this.redirectUriResolver.resolve(this.resolveRequestOrigin(request)),
|
|
40
|
+
});
|
|
28
41
|
} catch (error) {
|
|
29
42
|
return ServerHttpErrorResponseFactory.fromUnknown(error);
|
|
30
43
|
}
|
|
31
44
|
}
|
|
32
45
|
|
|
33
|
-
async
|
|
46
|
+
async postDisconnect(request: Request): Promise<Response> {
|
|
34
47
|
try {
|
|
35
48
|
const url = new URL(request.url);
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return new Response(this.createPopupHtml({ kind: "oauth2.connected", ...result }), {
|
|
42
|
-
headers: {
|
|
43
|
-
"content-type": "text/html; charset=utf-8",
|
|
44
|
-
},
|
|
45
|
-
});
|
|
49
|
+
const instanceId = url.searchParams.get("instanceId")?.trim();
|
|
50
|
+
if (!instanceId) {
|
|
51
|
+
return Response.json({ error: "Missing instanceId query parameter." }, { status: 400 });
|
|
52
|
+
}
|
|
53
|
+
return Response.json(await this.credentialInstanceService.disconnectOAuth2(instanceId));
|
|
46
54
|
} catch (error) {
|
|
47
|
-
|
|
48
|
-
return new Response(this.createPopupHtml({ kind: "oauth2.error", message }), {
|
|
49
|
-
status: 400,
|
|
50
|
-
headers: {
|
|
51
|
-
"content-type": "text/html; charset=utf-8",
|
|
52
|
-
},
|
|
53
|
-
});
|
|
55
|
+
return ServerHttpErrorResponseFactory.fromUnknown(error);
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
async
|
|
59
|
+
async postOAuthStart(request: Request): Promise<Response> {
|
|
58
60
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
const body = await HttpRequestJsonBodyReader.readJsonBody<OAuthStartRequestBody>(request);
|
|
62
|
+
if (!body.typeId?.trim()) {
|
|
63
|
+
return Response.json({ error: "Missing required field: typeId" }, { status: 400 });
|
|
64
|
+
}
|
|
65
|
+
if (!body.instanceId?.trim()) {
|
|
66
|
+
return Response.json({ error: "Missing required field: instanceId" }, { status: 400 });
|
|
67
|
+
}
|
|
68
|
+
if (!body.redirectUri?.trim()) {
|
|
69
|
+
return Response.json({ error: "Missing required field: redirectUri" }, { status: 400 });
|
|
70
|
+
}
|
|
71
|
+
const result = await this.oauthFlowExecutor.start({
|
|
72
|
+
typeId: body.typeId.trim(),
|
|
73
|
+
instanceId: body.instanceId.trim(),
|
|
74
|
+
redirectUri: body.redirectUri.trim(),
|
|
75
|
+
scopes: body.scopes ?? [],
|
|
61
76
|
});
|
|
77
|
+
return Response.json({ consentUrl: result.consentUrl, stateToken: result.stateToken });
|
|
62
78
|
} catch (error) {
|
|
63
79
|
return ServerHttpErrorResponseFactory.fromUnknown(error);
|
|
64
80
|
}
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
async
|
|
83
|
+
async getOAuthCallback(request: Request): Promise<Response> {
|
|
68
84
|
try {
|
|
69
85
|
const url = new URL(request.url);
|
|
70
|
-
const
|
|
86
|
+
const code = url.searchParams.get("code")?.trim();
|
|
87
|
+
const stateToken = url.searchParams.get("state")?.trim();
|
|
88
|
+
if (!code || !stateToken) {
|
|
89
|
+
return new Response(
|
|
90
|
+
this.createPopupHtml({ kind: "oauth2.error", message: "Missing code or state parameter." }),
|
|
91
|
+
{
|
|
92
|
+
status: 400,
|
|
93
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const instanceId = this.oauthFlowExecutor.lookupInstanceId(stateToken);
|
|
71
98
|
if (!instanceId) {
|
|
72
|
-
return Response
|
|
99
|
+
return new Response(
|
|
100
|
+
this.createPopupHtml({ kind: "oauth2.error", message: "OAuth state token not found or already used." }),
|
|
101
|
+
{ status: 400, headers: { "content-type": "text/html; charset=utf-8" } },
|
|
102
|
+
);
|
|
73
103
|
}
|
|
74
|
-
|
|
104
|
+
const material = await this.oauthFlowExecutor.completeCallback({ stateToken, code });
|
|
105
|
+
const nowIso = new Date().toISOString();
|
|
106
|
+
const encryptedMaterial = this.credentialSecretCipher.encrypt({
|
|
107
|
+
accessToken: material.accessToken,
|
|
108
|
+
refreshToken: material.refreshToken ?? null,
|
|
109
|
+
expiresAt: material.expiresAt ?? null,
|
|
110
|
+
grantedScopes: material.grantedScopes.join(" "),
|
|
111
|
+
});
|
|
112
|
+
await this.credentialStore.saveOAuth2Material({
|
|
113
|
+
instanceId,
|
|
114
|
+
encryptedJson: encryptedMaterial.encryptedJson,
|
|
115
|
+
encryptionKeyId: encryptedMaterial.encryptionKeyId,
|
|
116
|
+
schemaVersion: encryptedMaterial.schemaVersion,
|
|
117
|
+
metadata: {
|
|
118
|
+
providerId: "local",
|
|
119
|
+
connectedAt: nowIso,
|
|
120
|
+
scopes: [...material.grantedScopes],
|
|
121
|
+
updatedAt: nowIso,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
await this.credentialInstanceService.markOAuth2Connected(instanceId, nowIso);
|
|
125
|
+
return new Response(this.createPopupHtml({ kind: "oauth2.connected", instanceId }), {
|
|
126
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
127
|
+
});
|
|
75
128
|
} catch (error) {
|
|
76
|
-
|
|
129
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
130
|
+
return new Response(this.createPopupHtml({ kind: "oauth2.error", message }), {
|
|
131
|
+
status: 400,
|
|
132
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
133
|
+
});
|
|
77
134
|
}
|
|
78
135
|
}
|
|
79
136
|
|
|
@@ -6,6 +6,7 @@ import type { NamespacedUnregister } from "tsx/esm/api";
|
|
|
6
6
|
import type { CodemationConfig } from "../config/CodemationConfig";
|
|
7
7
|
import { CodemationConfigNormalizer } from "../config/CodemationConfigNormalizer";
|
|
8
8
|
import type { NormalizedCodemationConfig } from "../config/CodemationConfigNormalizer";
|
|
9
|
+
import { BootTimer } from "../../bootstrap/perf/BootTimer";
|
|
9
10
|
import { logLevelPolicyFactory } from "../../infrastructure/logging/LogLevelPolicyFactory";
|
|
10
11
|
import { ServerLoggerFactory } from "../../infrastructure/logging/ServerLoggerFactory";
|
|
11
12
|
import { DiscoveredWorkflowsEmptyMessageFactory } from "./DiscoveredWorkflowsEmptyMessageFactory";
|
|
@@ -37,9 +38,45 @@ export class CodemationConsumerConfigLoader {
|
|
|
37
38
|
private readonly performanceDiagnosticsLogger = new ServerLoggerFactory(
|
|
38
39
|
logLevelPolicyFactory,
|
|
39
40
|
).createPerformanceDiagnostics("codemation-config-loader.timing");
|
|
41
|
+
/**
|
|
42
|
+
* In-flight + completed load promises keyed by `${consumerRoot}|${configPathOverride}`. The
|
|
43
|
+
* boot path constructs MULTIPLE CodemationConsumerConfigLoader instances (one inside the CLI's
|
|
44
|
+
* DatabaseMigrationsApplyService, another inside NextHostEdgeSeedLoader, another inside
|
|
45
|
+
* AppConfigLoader for the disposable runtime) and each independently calls `load(...)`. Without
|
|
46
|
+
* a cache shared across instances, the same `${consumerRoot}` ends up importing
|
|
47
|
+
* codemation.config.ts + discovered workflow modules ~3 times for a single dev boot. The cache
|
|
48
|
+
* has to be static so it spans every loader instance in the process.
|
|
49
|
+
*
|
|
50
|
+
* Callers MUST invoke `invalidateAll()` on a source-change reload — the dev source watcher
|
|
51
|
+
* already tears the runtime down and reboots; it just needs to clear this map first.
|
|
52
|
+
*/
|
|
53
|
+
private static readonly resolutionCache = new Map<string, Promise<CodemationConsumerConfigResolution>>();
|
|
54
|
+
|
|
55
|
+
static invalidateAll(): void {
|
|
56
|
+
this.resolutionCache.clear();
|
|
57
|
+
}
|
|
40
58
|
|
|
41
59
|
async load(
|
|
42
60
|
args: Readonly<{ consumerRoot: string; configPathOverride?: string }>,
|
|
61
|
+
): Promise<CodemationConsumerConfigResolution> {
|
|
62
|
+
const cacheKey = `${args.consumerRoot}|${args.configPathOverride ?? ""}`;
|
|
63
|
+
const cached = CodemationConsumerConfigLoader.resolutionCache.get(cacheKey);
|
|
64
|
+
if (cached) {
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
const promise = this.loadUncached(args);
|
|
68
|
+
CodemationConsumerConfigLoader.resolutionCache.set(cacheKey, promise);
|
|
69
|
+
try {
|
|
70
|
+
return await promise;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// A failed load shouldn't poison the cache — future retries should re-attempt.
|
|
73
|
+
CodemationConsumerConfigLoader.resolutionCache.delete(cacheKey);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async loadUncached(
|
|
79
|
+
args: Readonly<{ consumerRoot: string; configPathOverride?: string }>,
|
|
43
80
|
): Promise<CodemationConsumerConfigResolution> {
|
|
44
81
|
const loadStarted = performance.now();
|
|
45
82
|
let mark = loadStarted;
|
|
@@ -54,25 +91,33 @@ export class CodemationConsumerConfigLoader {
|
|
|
54
91
|
`load.${label} +${delta.toFixed(1)}ms (cumulative ${(now - loadStarted).toFixed(1)}ms)`,
|
|
55
92
|
);
|
|
56
93
|
};
|
|
57
|
-
const bootstrapSource = await
|
|
94
|
+
const bootstrapSource = await BootTimer.measureAsync("config.resolveConfigPath", () =>
|
|
95
|
+
this.resolveConfigPath(args.consumerRoot, args.configPathOverride),
|
|
96
|
+
);
|
|
58
97
|
phaseMs("resolveConfigPath");
|
|
59
98
|
if (!bootstrapSource) {
|
|
60
99
|
throw new Error(
|
|
61
100
|
'Codemation config not found. Expected "codemation.config.ts" in the consumer project root or "src/".',
|
|
62
101
|
);
|
|
63
102
|
}
|
|
64
|
-
const moduleExports = await
|
|
103
|
+
const moduleExports = await BootTimer.measureAsync("config.importConfigModule", () =>
|
|
104
|
+
this.importModule(bootstrapSource, importSession),
|
|
105
|
+
);
|
|
65
106
|
phaseMs("importConfigModule");
|
|
66
107
|
const rawConfig = this.configExportsResolver.resolveConfig(moduleExports);
|
|
67
108
|
if (!rawConfig) {
|
|
68
109
|
throw new Error(`Config file does not export a Codemation config object: ${bootstrapSource}`);
|
|
69
110
|
}
|
|
70
111
|
const config = this.configNormalizer.normalize(rawConfig);
|
|
71
|
-
const workflowSources = await
|
|
112
|
+
const workflowSources = await BootTimer.measureAsync("config.resolveWorkflowSources", () =>
|
|
113
|
+
this.resolveWorkflowSources(args.consumerRoot, config),
|
|
114
|
+
);
|
|
72
115
|
phaseMs("resolveWorkflowSources");
|
|
73
|
-
const workflows =
|
|
74
|
-
|
|
75
|
-
|
|
116
|
+
const workflows = await BootTimer.measureAsync("config.loadDiscoveredWorkflows", async () =>
|
|
117
|
+
this.mergeWorkflows(
|
|
118
|
+
config.workflows ?? [],
|
|
119
|
+
await this.loadDiscoveredWorkflows(args.consumerRoot, config, workflowSources, importSession),
|
|
120
|
+
),
|
|
76
121
|
);
|
|
77
122
|
phaseMs("loadDiscoveredWorkflows");
|
|
78
123
|
const resolvedConfig: NormalizedCodemationConfig = {
|
|
@@ -145,7 +190,9 @@ export class CodemationConsumerConfigLoader {
|
|
|
145
190
|
workflowDiscoveryDirectories,
|
|
146
191
|
absoluteWorkflowModulePath: workflowSource,
|
|
147
192
|
}),
|
|
148
|
-
moduleExports: await
|
|
193
|
+
moduleExports: await BootTimer.measureAsync(`workflow.${path.basename(workflowSource).replace(/\.tsx?$/, "")}`, () =>
|
|
194
|
+
this.importModule(workflowSource, importSession),
|
|
195
|
+
),
|
|
149
196
|
})),
|
|
150
197
|
);
|
|
151
198
|
for (const loadedWorkflowModule of loadedWorkflowModules) {
|
|
@@ -124,6 +124,7 @@ export class CodemationPluginDiscovery {
|
|
|
124
124
|
}
|
|
125
125
|
const pluginValue = value as {
|
|
126
126
|
credentialTypes?: unknown;
|
|
127
|
+
mcpServers?: unknown;
|
|
127
128
|
register?: unknown;
|
|
128
129
|
sandbox?: unknown;
|
|
129
130
|
};
|
|
@@ -133,9 +134,13 @@ export class CodemationPluginDiscovery {
|
|
|
133
134
|
if (pluginValue.credentialTypes !== undefined && !Array.isArray(pluginValue.credentialTypes)) {
|
|
134
135
|
return false;
|
|
135
136
|
}
|
|
137
|
+
if (pluginValue.mcpServers !== undefined && !Array.isArray(pluginValue.mcpServers)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
136
140
|
return (
|
|
137
141
|
pluginValue.register !== undefined ||
|
|
138
142
|
pluginValue.credentialTypes !== undefined ||
|
|
143
|
+
pluginValue.mcpServers !== undefined ||
|
|
139
144
|
pluginValue.sandbox !== undefined ||
|
|
140
145
|
Object.keys(pluginValue).length === 0
|
|
141
146
|
);
|
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
import type { WorkflowDefinition } from "@codemation/core";
|
|
2
|
+
import { WorkflowEdgePortValidator } from "@codemation/core";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Collects exported values that match the {@link WorkflowDefinition} shape.
|
|
5
6
|
* Other exports (helpers, constants, type-only re-exports) are ignored.
|
|
7
|
+
*
|
|
8
|
+
* Throws if any workflow's edges reference output ports not declared by the
|
|
9
|
+
* source node config. All violations are reported at once so an agent can
|
|
10
|
+
* self-correct in a single pass.
|
|
6
11
|
*/
|
|
7
12
|
export class WorkflowDefinitionExportsResolver {
|
|
13
|
+
private readonly portValidator = new WorkflowEdgePortValidator();
|
|
14
|
+
|
|
8
15
|
resolve(moduleExports: Readonly<Record<string, unknown>>): ReadonlyArray<WorkflowDefinition> {
|
|
9
16
|
const workflows: WorkflowDefinition[] = [];
|
|
10
17
|
for (const exportedValue of Object.values(moduleExports)) {
|
|
11
18
|
if (this.isWorkflowDefinition(exportedValue)) {
|
|
19
|
+
this.validatePorts(exportedValue);
|
|
12
20
|
workflows.push(exportedValue);
|
|
13
21
|
}
|
|
14
22
|
}
|
|
15
23
|
return workflows;
|
|
16
24
|
}
|
|
17
25
|
|
|
26
|
+
private validatePorts(workflow: WorkflowDefinition): void {
|
|
27
|
+
const result = this.portValidator.validate(workflow);
|
|
28
|
+
if (!result.valid) {
|
|
29
|
+
const lines = result.errors.map((e) => ` - ${e.message}`).join("\n");
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Workflow "${workflow.id}" ("${workflow.name}") has ${result.errors.length} invalid edge port(s):\n${lines}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
private isWorkflowDefinition(value: unknown): value is WorkflowDefinition {
|
|
19
37
|
if (!value || typeof value !== "object") {
|
|
20
38
|
return false;
|
|
@@ -42,6 +42,17 @@ export class WorkflowModulePathFinder {
|
|
|
42
42
|
|
|
43
43
|
private isWorkflowModulePath(modulePath: string): boolean {
|
|
44
44
|
const extension = path.extname(modulePath);
|
|
45
|
-
|
|
45
|
+
if (!this.workflowExtensions.has(extension)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const basename = path.basename(modulePath);
|
|
49
|
+
if (basename.endsWith(".d.ts") || basename.endsWith(".d.mts")) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const withoutExt = basename.slice(0, -extension.length);
|
|
53
|
+
if (withoutExt.endsWith(".test") || withoutExt.endsWith(".spec")) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
46
57
|
}
|
|
47
58
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { injectable } from "@codemation/core";
|
|
2
|
+
import type { ManagedJwtVerifier, VerifiedManagedPrincipal } from "@codemation/managed-auth";
|
|
3
|
+
import type { WebsocketAuthenticator } from "./WebsocketAuthenticator.types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WebSocket authenticator for `auth.kind: "managed"`.
|
|
7
|
+
*
|
|
8
|
+
* Parses `?token=<jwt>` from the WS upgrade URL and delegates to
|
|
9
|
+
* `ManagedJwtVerifier`. Returns the verified principal on success or `null`
|
|
10
|
+
* when the token is missing, expired, has the wrong audience, or is otherwise
|
|
11
|
+
* invalid.
|
|
12
|
+
*
|
|
13
|
+
* Note: browsers cannot set `Authorization` headers on `new WebSocket(url)`,
|
|
14
|
+
* so the bearer is carried as a query-string parameter.
|
|
15
|
+
*/
|
|
16
|
+
@injectable()
|
|
17
|
+
export class ManagedWebsocketAuthenticator implements WebsocketAuthenticator {
|
|
18
|
+
constructor(private readonly verifier: ManagedJwtVerifier) {}
|
|
19
|
+
|
|
20
|
+
async authenticate(requestUrl: string | undefined): Promise<VerifiedManagedPrincipal | null> {
|
|
21
|
+
if (!requestUrl) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const token = this.extractToken(requestUrl);
|
|
26
|
+
if (!token) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = await this.verifier.verify(token);
|
|
31
|
+
if ("failure" in result) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private extractToken(requestUrl: string): string | null {
|
|
39
|
+
// requestUrl is a relative path like "/__codemation/internal/ws?token=..."
|
|
40
|
+
// Use a dummy base so URL can parse relative URLs.
|
|
41
|
+
let url: URL;
|
|
42
|
+
try {
|
|
43
|
+
url = new URL(requestUrl, "http://placeholder");
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const token = url.searchParams.get("token");
|
|
48
|
+
return token && token.length > 0 ? token : null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { VerifiedManagedPrincipal } from "@codemation/managed-auth";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authenticates an incoming WebSocket upgrade request.
|
|
5
|
+
*
|
|
6
|
+
* Implementations parse the upgrade URL (e.g. `?token=<jwt>`) and verify the
|
|
7
|
+
* credential. Returns the verified principal on success, or `null` when the
|
|
8
|
+
* request must be rejected with close-code 4401.
|
|
9
|
+
*/
|
|
10
|
+
export interface WebsocketAuthenticator {
|
|
11
|
+
authenticate(requestUrl: string | undefined): Promise<VerifiedManagedPrincipal | null>;
|
|
12
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { WebSocket, WebSocketServer } from "ws";
|
|
2
|
+
import type { IncomingMessage } from "node:http";
|
|
2
3
|
import type { WorkflowWebsocketMessage } from "../../application/contracts/WorkflowWebsocketMessage";
|
|
3
4
|
import type { Logger } from "../../application/logging/Logger";
|
|
4
5
|
import type { WorkflowWebsocketPublisher } from "../../application/websocket/WorkflowWebsocketPublisher";
|
|
5
6
|
import { ApiPaths } from "../http/ApiPaths";
|
|
7
|
+
import type { WebsocketAuthenticator } from "./WebsocketAuthenticator.types";
|
|
6
8
|
|
|
7
9
|
type WorkflowWebsocketClientMessage =
|
|
8
10
|
| Readonly<{ kind: "subscribe"; roomId: string }>
|
|
@@ -26,8 +28,18 @@ export class WorkflowWebsocketServer implements WorkflowWebsocketPublisher {
|
|
|
26
28
|
private readonly port: number,
|
|
27
29
|
private readonly bindHost: string,
|
|
28
30
|
private readonly logger: Logger,
|
|
31
|
+
private readonly authenticator: WebsocketAuthenticator | null = null,
|
|
29
32
|
) {}
|
|
30
33
|
|
|
34
|
+
/** Returns the actual port the server is listening on (useful when constructed with port 0). */
|
|
35
|
+
get listeningPort(): number {
|
|
36
|
+
const addr = this.websocketServer?.address();
|
|
37
|
+
if (!addr || typeof addr === "string") {
|
|
38
|
+
return this.port;
|
|
39
|
+
}
|
|
40
|
+
return addr.port;
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
async start(): Promise<void> {
|
|
32
44
|
if (this.started) {
|
|
33
45
|
return;
|
|
@@ -38,8 +50,8 @@ export class WorkflowWebsocketServer implements WorkflowWebsocketPublisher {
|
|
|
38
50
|
path: ApiPaths.workflowWebsocket(),
|
|
39
51
|
});
|
|
40
52
|
this.websocketServer = websocketServer;
|
|
41
|
-
websocketServer.on("connection", (socket) => {
|
|
42
|
-
void this.connect(socket);
|
|
53
|
+
websocketServer.on("connection", (socket, request) => {
|
|
54
|
+
void this.connect(socket, request);
|
|
43
55
|
});
|
|
44
56
|
try {
|
|
45
57
|
await this.awaitListening(websocketServer);
|
|
@@ -98,7 +110,16 @@ export class WorkflowWebsocketServer implements WorkflowWebsocketPublisher {
|
|
|
98
110
|
this.logger.debug(`published room=${roomId} sockets=${deliveredSocketCount} kind=${message.kind}`);
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
private async connect(socket: WebSocket): Promise<void> {
|
|
113
|
+
private async connect(socket: WebSocket, request: IncomingMessage): Promise<void> {
|
|
114
|
+
if (this.authenticator) {
|
|
115
|
+
const principal = await this.authenticator.authenticate(request.url);
|
|
116
|
+
if (!principal) {
|
|
117
|
+
this.logger.warn("websocket auth failed: closing with 4401");
|
|
118
|
+
socket.close(4401, "unauthorized");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
102
123
|
this.sockets.add(socket);
|
|
103
124
|
this.roomIdsBySocket.set(socket, new Set());
|
|
104
125
|
this.logger.debug(`client connected activeSockets=${this.sockets.size}`);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
import { execa, execaSync, type Options as ExecaOptions, type SyncOptions as ExecaSyncOptions } from "execa";
|
|
3
|
+
|
|
4
|
+
import type { ProcessRunner, ProcessRunOptions, ProcessRunResult } from "./ProcessRunner.types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Production {@link ProcessRunner}. Defers cross-platform executable resolution (`pnpm` ↔ `pnpm.cmd`,
|
|
8
|
+
* `.cmd` / `.bat` / `.ps1` shims on Windows) and argument quoting to execa so call sites stop having
|
|
9
|
+
* to hand-roll platform conditionals.
|
|
10
|
+
*/
|
|
11
|
+
export class ExecaProcessRunner implements ProcessRunner {
|
|
12
|
+
spawn(command: string, args: ReadonlyArray<string>, options?: ProcessRunOptions): ChildProcess {
|
|
13
|
+
return execa(command, [...args], this.toExecaOptions(options)) as unknown as ChildProcess;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
runSync(command: string, args: ReadonlyArray<string>, options?: ProcessRunOptions): ProcessRunResult {
|
|
17
|
+
const result = execaSync(command, [...args], this.toExecaSyncOptions(options));
|
|
18
|
+
return { exitCode: result.exitCode ?? null };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private toExecaOptions(options?: ProcessRunOptions): ExecaOptions {
|
|
22
|
+
return {
|
|
23
|
+
reject: false,
|
|
24
|
+
cwd: options?.cwd,
|
|
25
|
+
env: options?.env,
|
|
26
|
+
stdio: options?.stdio as ExecaOptions["stdio"],
|
|
27
|
+
detached: options?.detached,
|
|
28
|
+
windowsHide: options?.windowsHide,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private toExecaSyncOptions(options?: ProcessRunOptions): ExecaSyncOptions {
|
|
33
|
+
return {
|
|
34
|
+
reject: false,
|
|
35
|
+
cwd: options?.cwd,
|
|
36
|
+
env: options?.env,
|
|
37
|
+
stdio: options?.stdio as ExecaSyncOptions["stdio"],
|
|
38
|
+
windowsHide: options?.windowsHide,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export type ProcessRunOptions = Readonly<{
|
|
4
|
+
cwd?: string;
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
/**
|
|
7
|
+
* Mirrors `child_process.SpawnOptions["stdio"]`. The runner forwards the value verbatim to the
|
|
8
|
+
* underlying subprocess, so callers that need fine-grained per-fd control (e.g.
|
|
9
|
+
* `["ignore", "pipe", "pipe"]`) can pass a tuple.
|
|
10
|
+
*/
|
|
11
|
+
stdio?: "inherit" | "pipe" | "ignore" | ReadonlyArray<"inherit" | "pipe" | "ignore">;
|
|
12
|
+
/**
|
|
13
|
+
* On Unix this detaches the child from the parent's process group so it becomes the group
|
|
14
|
+
* leader (used by {@link DevTrackedProcessTreeKiller} to broadcast SIGTERM to descendants).
|
|
15
|
+
* On Windows it is ignored — `windowsHide` should be used to suppress console windows instead.
|
|
16
|
+
*/
|
|
17
|
+
detached?: boolean;
|
|
18
|
+
windowsHide?: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
export type ProcessRunResult = Readonly<{
|
|
22
|
+
exitCode: number | null;
|
|
23
|
+
}>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Cross-platform process spawning seam. Implementations resolve bare CLI names (`pnpm`, `prisma`,
|
|
27
|
+
* `next`, …) against the OS PATH using OS-appropriate executable lookup, so call sites stop having
|
|
28
|
+
* to remember `pnpm.cmd` or `shell: true` on Windows.
|
|
29
|
+
*
|
|
30
|
+
* `spawn` returns a Node `ChildProcess` so existing helpers like `DevNextChildProcessOutputFilter`
|
|
31
|
+
* and `DevTrackedProcessTreeKiller` keep working unchanged.
|
|
32
|
+
*/
|
|
33
|
+
export interface ProcessRunner {
|
|
34
|
+
/** Long-lived child (dev watcher, Next dev server). Returns a `ChildProcess`. */
|
|
35
|
+
spawn(command: string, args: ReadonlyArray<string>, options?: ProcessRunOptions): ChildProcess;
|
|
36
|
+
|
|
37
|
+
/** Synchronous one-shot (used by Prisma migrate deploy). */
|
|
38
|
+
runSync(command: string, args: ReadonlyArray<string>, options?: ProcessRunOptions): ProcessRunResult;
|
|
39
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { CodemationPostgresPrismaClientFactory } from "./persistenceServer";
|
|
2
2
|
export type { PrismaClient } from "./persistenceServer";
|
|
3
|
+
export { ExecaProcessRunner } from "./process/ExecaProcessRunner";
|
|
4
|
+
export type { ProcessRunner, ProcessRunOptions, ProcessRunResult } from "./process/ProcessRunner.types";
|
|
3
5
|
export { ApiPaths } from "./presentation/http/ApiPaths";
|
|
4
6
|
export { CodemationServerGateway } from "./presentation/http/CodemationServerGatewayFactory";
|
|
5
7
|
export { CodemationConsumerAppResolver } from "./presentation/server/CodemationConsumerAppResolver";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
import { ApplicationTokens } from "../applicationTokens";
|
|
4
|
+
import type { CommandBus } from "../application/bus/CommandBus";
|
|
5
|
+
import { SetWorkflowActivationCommand } from "../application/commands/SetWorkflowActivationCommand";
|
|
6
|
+
import { InternalHmacAuthMiddleware } from "../pairing/InternalHmacAuthMiddleware";
|
|
7
|
+
import type { InternalHonoApiRouteRegistrar } from "../presentation/http/hono/InternalHonoApiRouteRegistrar";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registers POST /internal/workflows/:workflowId/activation — HMAC-verified endpoint
|
|
11
|
+
* that activates or deactivates a workflow. Used by the coding agent to toggle workflow
|
|
12
|
+
* triggers without requiring a user session.
|
|
13
|
+
*/
|
|
14
|
+
@injectable()
|
|
15
|
+
export class InternalWorkflowActivationRegistrar implements InternalHonoApiRouteRegistrar {
|
|
16
|
+
constructor(
|
|
17
|
+
@inject(InternalHmacAuthMiddleware) private readonly hmacMiddleware: InternalHmacAuthMiddleware,
|
|
18
|
+
@inject(ApplicationTokens.CommandBus) private readonly commandBus: CommandBus,
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
register(app: Hono): void {
|
|
22
|
+
app.post("/internal/workflows/:workflowId/activation", this.hmacMiddleware.handle(), async (c) => {
|
|
23
|
+
const workflowId = c.req.param("workflowId");
|
|
24
|
+
let body: { active?: unknown };
|
|
25
|
+
try {
|
|
26
|
+
body = await c.req.json<{ active?: unknown }>();
|
|
27
|
+
} catch {
|
|
28
|
+
return c.json({ error: "Request body must be JSON with boolean active" }, 400);
|
|
29
|
+
}
|
|
30
|
+
if (typeof body.active !== "boolean") {
|
|
31
|
+
return c.json({ error: "Request body must include boolean active" }, 400);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const result = await this.commandBus.execute(new SetWorkflowActivationCommand(workflowId, body.active));
|
|
35
|
+
return c.json(result);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
+
return c.json({ error: message }, 500);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
import { ApplicationTokens } from "../applicationTokens";
|
|
4
|
+
import type { QueryBus } from "../application/bus/QueryBus";
|
|
5
|
+
import { GetWorkflowDetailQuery } from "../application/queries/GetWorkflowDetailQuery";
|
|
6
|
+
import { WorkflowDefinitionMapper } from "../application/mapping/WorkflowDefinitionMapper";
|
|
7
|
+
import { InternalHmacAuthMiddleware } from "../pairing/InternalHmacAuthMiddleware";
|
|
8
|
+
import type { InternalHonoApiRouteRegistrar } from "../presentation/http/hono/InternalHonoApiRouteRegistrar";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Registers GET /internal/workflows/:workflowId — HMAC-verified endpoint that returns a
|
|
12
|
+
* single workflow's full DAG (nodes + edges). Used by the concierge agent to inspect a
|
|
13
|
+
* specific workflow. Returns 404 (empty body) when the workflow id doesn't exist.
|
|
14
|
+
*/
|
|
15
|
+
@injectable()
|
|
16
|
+
export class InternalWorkflowDetailRegistrar implements InternalHonoApiRouteRegistrar {
|
|
17
|
+
constructor(
|
|
18
|
+
@inject(InternalHmacAuthMiddleware) private readonly hmacMiddleware: InternalHmacAuthMiddleware,
|
|
19
|
+
@inject(ApplicationTokens.QueryBus) private readonly queryBus: QueryBus,
|
|
20
|
+
@inject(WorkflowDefinitionMapper) private readonly mapper: WorkflowDefinitionMapper,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
register(app: Hono): void {
|
|
24
|
+
app.get("/internal/workflows/:workflowId", this.hmacMiddleware.handle(), async (c) => {
|
|
25
|
+
const workflowId = c.req.param("workflowId");
|
|
26
|
+
const workflow = await this.queryBus.execute(new GetWorkflowDetailQuery(workflowId));
|
|
27
|
+
if (!workflow) {
|
|
28
|
+
return c.body(null, 404);
|
|
29
|
+
}
|
|
30
|
+
return c.json(await this.mapper.map(workflow));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|