@codemation/host 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +483 -0
  2. package/dist/{ApiPaths-CLTHphYZ.js → ApiPaths-Dv1dcHu_.js} +4 -4
  3. package/dist/ApiPaths-Dv1dcHu_.js.map +1 -0
  4. package/dist/{AppConfigFactory-YnveXE9k.d.ts → AppConfigFactory-BT0y0LVC.d.ts} +8490 -5548
  5. package/dist/{AppConfigFactory-C6q-CSKb.js → AppConfigFactory-Cx4qQvRk.js} +112 -52
  6. package/dist/AppConfigFactory-Cx4qQvRk.js.map +1 -0
  7. package/dist/{AppContainerFactory-qaqc-R1D.js → AppContainerFactory-DRTjG7nG.js} +7298 -1732
  8. package/dist/AppContainerFactory-DRTjG7nG.js.map +1 -0
  9. package/dist/{CodemationAppContext-DRu1Dpri.d.ts → CodemationAppContext-CGFYVcSb.d.ts} +14 -4
  10. package/dist/{CodemationAuthoring.types-DZl-sJaM.js → CodemationAuthoring.types-BteaR3Dc.js} +19 -6
  11. package/dist/CodemationAuthoring.types-BteaR3Dc.js.map +1 -0
  12. package/dist/{CodemationAuthoring.types-fBRppnmi.d.ts → CodemationAuthoring.types-DiKKogum.d.ts} +30 -5
  13. package/dist/{CodemationConfigNormalizer-DVko3cVN.d.ts → CodemationConfigNormalizer-48f-T66P.d.ts} +3 -3
  14. package/dist/{CodemationConsumerConfigLoader-BeAUS144.js → CodemationConsumerConfigLoader-By-6tuGc.js} +81 -10
  15. package/dist/CodemationConsumerConfigLoader-By-6tuGc.js.map +1 -0
  16. package/dist/{CodemationConsumerConfigLoader-DJWr86f-.d.ts → CodemationConsumerConfigLoader-_PIYqwVx.d.ts} +18 -2
  17. package/dist/{CodemationPluginListMerger-B-W5Fa_X.js → CodemationPluginListMerger-D1B1IEbt.js} +1 -1
  18. package/dist/{CodemationPluginListMerger-B-W5Fa_X.js.map → CodemationPluginListMerger-D1B1IEbt.js.map} +1 -1
  19. package/dist/{CodemationPluginListMerger-DGc-jfa2.d.ts → CodemationPluginListMerger-DP7djJ9S.d.ts} +151 -19
  20. package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js +97 -0
  21. package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js.map +1 -0
  22. package/dist/{CodemationWhitelabelConfig-CWbcyQqn.d.ts → CodemationWhitelabelConfig-Ca2mCUeC.d.ts} +2 -2
  23. package/dist/{CollectionContracts.types-DdpHft0i.d.ts → CollectionContracts.types-DDyFYT_D.d.ts} +1 -1
  24. package/dist/{CredentialContractsRegistry-DrMIDSw8.d.ts → CredentialContractsRegistry-Bq2bq28t.d.ts} +2 -2
  25. package/dist/{CredentialServices-UfvHB-rN.d.ts → CredentialServices-BLloBztI.d.ts} +65 -20
  26. package/dist/{CredentialServices-CgxwguAv.js → CredentialServices-Dk8yypeL.js} +310 -51
  27. package/dist/CredentialServices-Dk8yypeL.js.map +1 -0
  28. package/dist/InternalHonoApiRouteRegistrar-c7t3KnV_.d.ts +17 -0
  29. package/dist/InternalPingRegistrar-DY3kSfxP.js +221 -0
  30. package/dist/InternalPingRegistrar-DY3kSfxP.js.map +1 -0
  31. package/dist/{ItemsInputNormalizer-C-KHg9Mo.d.ts → ItemsInputNormalizer-_RwIfRIQ.d.ts} +89 -25
  32. package/dist/{LogLevelPolicyFactory-CampWO0l.d.ts → LogLevelPolicyFactory-ewCHLDLn.d.ts} +2 -2
  33. package/dist/{PublicFrontendBootstrap-DzBgwOnG.d.ts → PublicFrontendBootstrap-Cev3qK46.d.ts} +9 -2
  34. package/dist/PublicFrontendBootstrapFactory-Dv04tJ-6.d.ts +82 -0
  35. package/dist/{PublicFrontendBootstrapJsonCodec-Cl_DLRh0.d.ts → PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts} +3 -3
  36. package/dist/{PublicFrontendBootstrapJsonCodec-DzqvA0uo.js → PublicFrontendBootstrapJsonCodec-CegIF_ne.js} +7 -2
  37. package/dist/PublicFrontendBootstrapJsonCodec-CegIF_ne.js.map +1 -0
  38. package/dist/ServerLoggerFactory-Ckk52S3w.js +223 -0
  39. package/dist/ServerLoggerFactory-Ckk52S3w.js.map +1 -0
  40. package/dist/{TelemetryContracts-DbaNomrH.d.ts → TelemetryContracts-BtDx84Cp.d.ts} +13 -4
  41. package/dist/{WorkflowPolicyUiPresentationFactory-DQEY-h_S.d.ts → WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts} +2 -2
  42. package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js} +1 -1
  43. package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js.map → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map} +1 -1
  44. package/dist/{WorkflowViewContracts-CzK2KFuz.d.ts → WorkflowViewContracts-B7aFQcIw.d.ts} +10 -1
  45. package/dist/authoring.d.ts +5 -5
  46. package/dist/authoring.js +1 -1
  47. package/dist/client.d.ts +4 -4
  48. package/dist/client.js +2 -2
  49. package/dist/consumer.d.ts +6 -6
  50. package/dist/consumer.js +2 -2
  51. package/dist/credentials.d.ts +6 -6
  52. package/dist/credentials.js +1 -1
  53. package/dist/devServerSidecar.d.ts +2 -2
  54. package/dist/devServerSidecar.js +1 -94
  55. package/dist/devServerSidecar.js.map +1 -1
  56. package/dist/dto.d.ts +6 -6
  57. package/dist/{index-BbBk26m0.d.ts → index-DilAYwnH.d.ts} +49 -3
  58. package/dist/index.d.ts +141 -21
  59. package/dist/index.js +109 -14
  60. package/dist/index.js.map +1 -0
  61. package/dist/mapping.d.ts +2 -2
  62. package/dist/mapping.js +1 -1
  63. package/dist/nextServer.d.ts +42 -113
  64. package/dist/nextServer.js +9 -7
  65. package/dist/pairing.d.ts +93 -0
  66. package/dist/pairing.js +5 -0
  67. package/dist/pairing.types-snfZ_OzB.d.ts +19 -0
  68. package/dist/persistenceServer-B71RGvSj.d.ts +30 -0
  69. package/dist/{persistenceServer-CmsIKnO9.js → persistenceServer-C-hH4z6l.js} +2 -2
  70. package/dist/{persistenceServer-CmsIKnO9.js.map → persistenceServer-C-hH4z6l.js.map} +1 -1
  71. package/dist/persistenceServer.d.ts +8 -8
  72. package/dist/persistenceServer.js +3 -3
  73. package/dist/{server-MUNGsBYK.d.ts → server-09PKasWR.d.ts} +21 -6
  74. package/dist/{server-CJFfY67o.js → server-vtRCPgRJ.js} +7 -6
  75. package/dist/{server-CJFfY67o.js.map → server-vtRCPgRJ.js.map} +1 -1
  76. package/dist/server.d.ts +14 -14
  77. package/dist/server.js +13 -11
  78. package/package.json +47 -58
  79. package/prisma/migrations/20260519000000_workflow_audit_log/migration.sql +23 -0
  80. package/prisma/migrations/20260519100000_storage_growth_fixes/migration.sql +61 -0
  81. package/prisma/migrations.sqlite/20260519000000_workflow_audit_log/migration.sql +21 -0
  82. package/prisma/migrations.sqlite/20260519100000_storage_growth_fixes/migration.sql +29 -0
  83. package/prisma/schema.postgresql.prisma +55 -17
  84. package/prisma/schema.sqlite.prisma +55 -17
  85. package/prisma-generated/prisma-postgresql-client/edge.js +33 -5
  86. package/prisma-generated/prisma-postgresql-client/index-browser.js +29 -1
  87. package/prisma-generated/prisma-postgresql-client/index.d.ts +8933 -5716
  88. package/prisma-generated/prisma-postgresql-client/index.js +33 -5
  89. package/prisma-generated/prisma-postgresql-client/package.json +1 -1
  90. package/prisma-generated/prisma-postgresql-client/schema.prisma +38 -0
  91. package/prisma-generated/prisma-sqlite-client/edge.js +33 -5
  92. package/prisma-generated/prisma-sqlite-client/index-browser.js +29 -1
  93. package/prisma-generated/prisma-sqlite-client/index.d.ts +8925 -5713
  94. package/prisma-generated/prisma-sqlite-client/index.js +33 -5
  95. package/prisma-generated/prisma-sqlite-client/package.json +1 -1
  96. package/prisma-generated/prisma-sqlite-client/schema.prisma +38 -0
  97. package/scripts/check-collections.mjs +18 -0
  98. package/scripts/generate-prisma-clients.mjs +20 -11
  99. package/src/application/WorkflowAuditLogPruneScheduler.ts +96 -0
  100. package/src/application/auth/AuthenticatedPrincipal.ts +4 -0
  101. package/src/application/commands/StartWorkflowRunCommandHandler.ts +4 -0
  102. package/src/application/contracts/WorkflowViewContracts.ts +6 -0
  103. package/src/application/contracts/WorkflowWebsocketMessage.ts +3 -1
  104. package/src/application/mapping/WorkflowDefinitionMapper.ts +40 -1
  105. package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +7 -1
  106. package/src/application/telemetry/OtelExecutionTelemetry.types.ts +5 -0
  107. package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +4 -0
  108. package/src/application/telemetry/StoredTelemetrySpanScope.ts +6 -2
  109. package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +27 -17
  110. package/src/application/telemetry/TelemetrySpanPublisher.ts +11 -0
  111. package/src/application/websocket/TelemetrySpanWebsocketRelay.ts +31 -0
  112. package/src/applicationTokens.ts +20 -1
  113. package/src/audit/IAuditEmitter.ts +32 -0
  114. package/src/audit/PrismaWorkflowAuditLogRepository.ts +34 -0
  115. package/src/audit/WorkflowAuditLogWriter.ts +125 -0
  116. package/src/auth/managed/ManagedAuthConfig.ts +29 -0
  117. package/src/auth/managed/ManagedAuthMiddleware.ts +52 -0
  118. package/src/auth/managed/ManagedCorsMiddleware.ts +43 -0
  119. package/src/auth/managed/ManagedModeBootGuard.ts +27 -0
  120. package/src/auth/managed/index.ts +5 -0
  121. package/src/bootstrap/AppContainerFactory.ts +295 -29
  122. package/src/bootstrap/AppContainerLifecycle.ts +31 -0
  123. package/src/bootstrap/perf/BootTimer.ts +168 -0
  124. package/src/bootstrap/runtime/AppConfigFactory.ts +21 -65
  125. package/src/bootstrap/runtime/FrontendRuntime.ts +4 -1
  126. package/src/bootstrap/runtime/HeadlessApiRuntime.ts +47 -0
  127. package/src/bootstrap/runtime/WorkerRuntime.ts +2 -1
  128. package/src/credentials/BrokerClient.ts +49 -0
  129. package/src/credentials/BrokerRefreshError.ts +12 -0
  130. package/src/credentials/BrokerRefreshInvalidGrantError.ts +13 -0
  131. package/src/credentials/ControlPlaneCatalogFetcher.ts +261 -0
  132. package/src/credentials/CredentialOAuth2MaterialReader.ts +136 -0
  133. package/src/credentials/InternalCredentialsListRegistrar.ts +48 -0
  134. package/src/credentials/InternalCredentialsPushRegistrar.ts +125 -0
  135. package/src/credentials/LocalOAuthFlowExecutor.ts +316 -0
  136. package/src/credentials/ManagedOAuthFlowExecutor.ts +94 -0
  137. package/src/credentials/ManagedOAuthRefreshInvalidGrantError.ts +13 -0
  138. package/src/credentials/catalogTypes.ts +4 -0
  139. package/src/credentials/refresh/CredentialDisconnectedError.ts +11 -0
  140. package/src/domain/credentials/CredentialBindingService.ts +54 -2
  141. package/src/domain/credentials/CredentialKeyRotatedError.ts +22 -0
  142. package/src/domain/credentials/CredentialSecretCipher.ts +68 -6
  143. package/src/domain/credentials/CredentialTypeRegistryImpl.ts +117 -10
  144. package/src/domain/credentials/OAuth2RedirectUriResolver.ts +79 -0
  145. package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +14 -5
  146. package/src/domain/telemetry/TelemetryContracts.ts +7 -1
  147. package/src/domain/workflows/WorkflowActivationPreflight.ts +24 -1
  148. package/src/domain/workflows/WorkflowActivationPreflightRules.ts +40 -1
  149. package/src/index.ts +9 -0
  150. package/src/infrastructure/binary/LocalFilesystemBinaryStorageRegistry.ts +29 -1
  151. package/src/infrastructure/binary/S3BinaryStorage.ts +169 -0
  152. package/src/infrastructure/binary/S3BinaryStorageConfig.ts +17 -0
  153. package/src/infrastructure/config/CodemationPluginRegistrar.ts +3 -1
  154. package/src/infrastructure/persistence/CodemationDatabaseUrlParser.ts +41 -0
  155. package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +8 -3
  156. package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +21 -13
  157. package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +43 -8
  158. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +26 -3
  159. package/src/infrastructure/persistence/PrismaWorkflowSnapshotRepository.ts +48 -0
  160. package/src/mcp/AgentMcpIntegrationImpl.ts +344 -0
  161. package/src/mcp/McpClientFactory.ts +29 -0
  162. package/src/mcp/McpConnectionPool.ts +184 -0
  163. package/src/mcp/McpConnectionPool.types.ts +12 -0
  164. package/src/mcp/McpServerCatalog.ts +104 -0
  165. package/src/mcp/index.ts +5 -0
  166. package/src/pairing/HmacRequestSigner.ts +32 -0
  167. package/src/pairing/IncomingHmacVerifier.ts +82 -0
  168. package/src/pairing/InternalHmacAuthMiddleware.ts +33 -0
  169. package/src/pairing/InternalPingRegistrar.ts +25 -0
  170. package/src/pairing/PairedFetch.ts +33 -0
  171. package/src/pairing/PairingConfigFactory.ts +35 -0
  172. package/src/pairing/PairingConfigToken.ts +6 -0
  173. package/src/pairing/index.ts +14 -0
  174. package/src/pairing/pairing.types.ts +18 -0
  175. package/src/pairing.ts +17 -0
  176. package/src/persistenceServer.ts +1 -0
  177. package/src/presentation/config/AppConfig.ts +7 -1
  178. package/src/presentation/config/CodemationAuthConfig.ts +1 -1
  179. package/src/presentation/config/CodemationAuthoring.types.ts +60 -5
  180. package/src/presentation/config/CodemationConfig.ts +9 -0
  181. package/src/presentation/config/CodemationConfigNormalizer.ts +39 -1
  182. package/src/presentation/config/CodemationPlugin.ts +2 -1
  183. package/src/presentation/frontend/CodemationFrontendAuthSnapshot.ts +5 -0
  184. package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -1
  185. package/src/presentation/frontend/PublicFrontendBootstrap.ts +2 -0
  186. package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +5 -1
  187. package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +4 -1
  188. package/src/presentation/http/ApiPaths.ts +4 -4
  189. package/src/presentation/http/HeadlessHttpServerFactory.ts +56 -0
  190. package/src/presentation/http/ServerHttpErrorResponseFactory.ts +39 -2
  191. package/src/presentation/http/hono/CodemationHonoApiAppFactory.ts +33 -8
  192. package/src/presentation/http/hono/InternalHonoApiRouteRegistrar.ts +12 -0
  193. package/src/presentation/http/hono/registrars/ManagedMeHonoApiRouteRegistrar.ts +35 -0
  194. package/src/presentation/http/hono/registrars/OAuth2HonoApiRouteRegistrar.ts +2 -2
  195. package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +28 -0
  196. package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +98 -41
  197. package/src/presentation/server/CodemationConsumerConfigLoader.ts +59 -7
  198. package/src/presentation/server/CodemationPluginDiscovery.ts +5 -0
  199. package/src/presentation/server/WorkflowDefinitionExportsResolver.ts +18 -0
  200. package/src/presentation/server/WorkflowModulePathFinder.ts +12 -1
  201. package/src/presentation/websocket/ManagedWebsocketAuthenticator.ts +50 -0
  202. package/src/presentation/websocket/WebsocketAuthenticator.types.ts +12 -0
  203. package/src/presentation/websocket/WorkflowWebsocketServer.ts +24 -3
  204. package/src/presentation/websocket/WorkflowWebsocketServerFactory.ts +16 -0
  205. package/src/process/ExecaProcessRunner.ts +41 -0
  206. package/src/process/ProcessRunner.types.ts +39 -0
  207. package/src/server.ts +2 -0
  208. package/src/workflows/InternalWorkflowActivationRegistrar.ts +42 -0
  209. package/src/workflows/InternalWorkflowDetailRegistrar.ts +33 -0
  210. package/src/workflows/InternalWorkflowTestRunRegistrar.ts +91 -0
  211. package/src/workflows/InternalWorkflowsListRegistrar.ts +28 -0
  212. package/src/workflows/discovery/WorkflowDirectoryDiscoverer.ts +79 -0
  213. package/tsconfig.json +2 -0
  214. package/vitest.shared.ts +5 -0
  215. package/dist/ApiPaths-CLTHphYZ.js.map +0 -1
  216. package/dist/AppConfigFactory-C6q-CSKb.js.map +0 -1
  217. package/dist/AppContainerFactory-qaqc-R1D.js.map +0 -1
  218. package/dist/CodemationAuthoring.types-DZl-sJaM.js.map +0 -1
  219. package/dist/CodemationConsumerConfigLoader-BeAUS144.js.map +0 -1
  220. package/dist/CredentialServices-CgxwguAv.js.map +0 -1
  221. package/dist/PublicFrontendBootstrapFactory-Cb2pLmDd.d.ts +0 -45
  222. package/dist/PublicFrontendBootstrapJsonCodec-DzqvA0uo.js.map +0 -1
  223. package/dist/ServerLoggerFactory-BKSIh9Xv.js +0 -98
  224. package/dist/ServerLoggerFactory-BKSIh9Xv.js.map +0 -1
  225. package/dist/persistenceServer-vtJAGDat.d.ts +0 -9
  226. package/src/domain/credentials/OAuth2ConnectServiceFactory.ts +0 -411
@@ -15,7 +15,7 @@ export class PublicFrontendBootstrapJsonCodec {
15
15
  if (!parsed || typeof parsed !== "object") {
16
16
  return null;
17
17
  }
18
- return {
18
+ const base: PublicFrontendBootstrap = {
19
19
  credentialsEnabled: parsed.credentialsEnabled === true,
20
20
  logoUrl: typeof parsed.logoUrl === "string" && parsed.logoUrl.trim().length > 0 ? parsed.logoUrl : null,
21
21
  oauthProviders: this.resolveOauthProviders(parsed.oauthProviders),
@@ -25,6 +25,9 @@ export class PublicFrontendBootstrapJsonCodec {
25
25
  : "Codemation",
26
26
  uiAuthEnabled: parsed.uiAuthEnabled !== false,
27
27
  };
28
+ const cpWebOrigin =
29
+ typeof parsed.cpWebOrigin === "string" && parsed.cpWebOrigin.trim().length > 0 ? parsed.cpWebOrigin : undefined;
30
+ return cpWebOrigin ? { ...base, cpWebOrigin } : base;
28
31
  } catch {
29
32
  return null;
30
33
  }
@@ -145,10 +145,6 @@ export class ApiPaths {
145
145
  return `${this.apiBasePath}/credential-bindings`;
146
146
  }
147
147
 
148
- static oauth2Auth(instanceId: string): string {
149
- return `${this.oauth2BasePath}/auth?instanceId=${encodeURIComponent(instanceId)}`;
150
- }
151
-
152
148
  static oauth2RedirectUri(): string {
153
149
  return `${this.oauth2BasePath}/redirect-uri`;
154
150
  }
@@ -157,6 +153,10 @@ export class ApiPaths {
157
153
  return `${this.oauth2BasePath}/disconnect?instanceId=${encodeURIComponent(instanceId)}`;
158
154
  }
159
155
 
156
+ static credentialOAuthStart(): string {
157
+ return `${this.credentialsBasePath}/oauth/start`;
158
+ }
159
+
160
160
  static workflowWebsocket(): string {
161
161
  return `${this.workflowsBasePath}/ws`;
162
162
  }
@@ -0,0 +1,56 @@
1
+ import { createServer, type Server } from "node:http";
2
+ import type { Logger } from "../../application/logging/Logger";
3
+ import type { CodemationHonoApiApp } from "./hono/CodemationHonoApiAppFactory";
4
+
5
+ /**
6
+ * Creates a Node.js http.Server that bridges IncomingMessage to Hono's Fetch API.
7
+ * Used by {@link import("../../bootstrap/runtime/HeadlessApiRuntime").HeadlessApiRuntime}
8
+ * to serve the Hono API without Next.js.
9
+ */
10
+ export class HeadlessHttpServerFactory {
11
+ create(honoApp: CodemationHonoApiApp, port: number, logger: Logger): Server {
12
+ return createServer((req, res) => {
13
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? `127.0.0.1:${port}`}`);
14
+ const headers = new Headers();
15
+ for (const [key, value] of Object.entries(req.headers)) {
16
+ if (value === undefined) continue;
17
+ if (Array.isArray(value)) {
18
+ for (const v of value) headers.append(key, v);
19
+ } else {
20
+ headers.set(key, value);
21
+ }
22
+ }
23
+ const chunks: Buffer[] = [];
24
+ req.on("data", (chunk: Buffer) => chunks.push(chunk));
25
+ req.on("end", () => {
26
+ // eslint-disable-next-line codemation/no-buffer-everything -- node:http bridge; no streaming alternative when adapting IncomingMessage to Fetch API Request
27
+ const body = chunks.length > 0 ? Buffer.concat(chunks) : null;
28
+ const fetchRequest = new Request(url, {
29
+ method: req.method ?? "GET",
30
+ headers,
31
+ body: body?.byteLength ? body : undefined,
32
+ // @ts-expect-error — Node's Request needs duplex for streaming; required in some runtimes
33
+ duplex: "half",
34
+ });
35
+ Promise.resolve(honoApp.fetch(fetchRequest))
36
+ .then(async (fetchResponse: Response) => {
37
+ const responseHeaders: Record<string, string> = {};
38
+ fetchResponse.headers.forEach((value, key) => {
39
+ responseHeaders[key] = value;
40
+ });
41
+ res.writeHead(fetchResponse.status, responseHeaders);
42
+ // eslint-disable-next-line codemation/no-buffer-everything -- node:http bridge; Hono Fetch Response must be fully buffered to write to ServerResponse
43
+ const responseBody = await fetchResponse.arrayBuffer();
44
+ res.end(Buffer.from(responseBody));
45
+ })
46
+ .catch((err: unknown) => {
47
+ logger.error("Unhandled request error", err instanceof Error ? err : new Error(String(err)));
48
+ if (!res.headersSent) {
49
+ res.writeHead(500);
50
+ res.end("Internal server error");
51
+ }
52
+ });
53
+ });
54
+ });
55
+ }
56
+ }
@@ -1,13 +1,50 @@
1
1
  import { ApplicationRequestError } from "../../application/ApplicationRequestError";
2
2
 
3
+ /**
4
+ * Shape of the JSON body returned on an unhandled 500. The canvas (and any other client)
5
+ * reads `message` + optional `stack` + optional `cause` to surface a copy/pastable error
6
+ * dialog. Generic "Internal server error" with no detail makes operator triage impossible
7
+ * — this contract preserves the diagnostic information the CLI logs anyway.
8
+ */
9
+ export type ServerHttpUnhandledErrorPayload = Readonly<{
10
+ error: "Internal server error";
11
+ message: string;
12
+ name?: string;
13
+ stack?: string;
14
+ cause?: string;
15
+ }>;
16
+
3
17
  export class ServerHttpErrorResponseFactory {
4
18
  static fromUnknown(error: unknown): Response {
5
19
  if (error instanceof ApplicationRequestError) {
6
20
  return Response.json(error.payload, { status: error.status });
7
21
  }
8
22
  this.logUnexpectedError(error);
9
- const message = error instanceof Error ? error.message : String(error);
10
- return Response.json({ error: message }, { status: 500 });
23
+ return Response.json(this.toUnhandledPayload(error), { status: 500 });
24
+ }
25
+
26
+ private static toUnhandledPayload(error: unknown): ServerHttpUnhandledErrorPayload {
27
+ if (error instanceof Error) {
28
+ return {
29
+ error: "Internal server error",
30
+ message: error.message || `${error.name}: <no message>`,
31
+ name: error.name,
32
+ stack: error.stack,
33
+ cause: this.formatCauseValue(error),
34
+ };
35
+ }
36
+ return { error: "Internal server error", message: String(error) };
37
+ }
38
+
39
+ private static formatCauseValue(error: Error): string | undefined {
40
+ if (!("cause" in error) || !error.cause) {
41
+ return undefined;
42
+ }
43
+ const cause = error.cause;
44
+ if (cause instanceof Error) {
45
+ return cause.stack ?? `${cause.name}: ${cause.message}`;
46
+ }
47
+ return String(cause);
11
48
  }
12
49
 
13
50
  private static logUnexpectedError(error: unknown): void {
@@ -5,7 +5,9 @@ import { ApplicationTokens } from "../../../applicationTokens";
5
5
  import { BinaryHttpRouteHandler } from "../routeHandlers/BinaryHttpRouteHandlerFactory";
6
6
  import { ServerHttpErrorResponseFactory } from "../ServerHttpErrorResponseFactory";
7
7
  import type { HonoApiRouteRegistrar } from "./HonoApiRouteRegistrar";
8
+ import type { InternalHonoApiRouteRegistrar } from "./InternalHonoApiRouteRegistrar";
8
9
  import { HonoHttpAnonymousRoutePolicy } from "./HonoHttpAnonymousRoutePolicyRegistry";
10
+ import { ManagedCorsMiddleware } from "../../../auth/managed/ManagedCorsMiddleware";
9
11
 
10
12
  @injectable()
11
13
  export class CodemationHonoApiApp {
@@ -18,10 +20,21 @@ export class CodemationHonoApiApp {
18
20
  registrars: ReadonlyArray<HonoApiRouteRegistrar>,
19
21
  @inject(BinaryHttpRouteHandler)
20
22
  binaryHttpRouteHandler: BinaryHttpRouteHandler,
23
+ @injectAll(ApplicationTokens.InternalHonoApiRouteRegistrar, { isOptional: true })
24
+ internalRegistrars: ReadonlyArray<InternalHonoApiRouteRegistrar>,
25
+ @injectAll(ApplicationTokens.ManagedCorsMiddleware, { isOptional: true })
26
+ corsMiddlewareList: ReadonlyArray<ManagedCorsMiddleware>,
21
27
  ) {
22
- const app = new Hono().basePath("/api");
23
- app.onError((error, _c) => ServerHttpErrorResponseFactory.fromUnknown(error));
24
- app.use("*", async (c, next) => {
28
+ // Root app composes /api/* (auth-gated) and /internal/* (HMAC-gated) sub-apps.
29
+ const root = new Hono();
30
+ const corsMiddleware = corsMiddlewareList[0] ?? null;
31
+ if (corsMiddleware) {
32
+ root.use("*", corsMiddleware.handle());
33
+ }
34
+
35
+ const api = new Hono().basePath("/api");
36
+ api.onError((error, _c) => ServerHttpErrorResponseFactory.fromUnknown(error));
37
+ api.use("*", async (c, next) => {
25
38
  if (HonoHttpAnonymousRoutePolicy.isAnonymousRoute(c.req.raw)) {
26
39
  await next();
27
40
  return;
@@ -33,25 +46,37 @@ export class CodemationHonoApiApp {
33
46
  await next();
34
47
  });
35
48
  for (const registrar of registrars) {
36
- registrar.register(app);
49
+ registrar.register(api);
37
50
  }
38
- app.get("/workflows/:workflowId/debugger-overlay/binary/:binaryId/content", (c) =>
51
+ api.get("/workflows/:workflowId/debugger-overlay/binary/:binaryId/content", (c) =>
39
52
  binaryHttpRouteHandler.getWorkflowOverlayBinaryContent(c.req.raw, {
40
53
  workflowId: c.req.param("workflowId"),
41
54
  binaryId: c.req.param("binaryId"),
42
55
  }),
43
56
  );
44
- app.post("/workflows/:workflowId/debugger-overlay/binary/upload", (c) =>
57
+ api.post("/workflows/:workflowId/debugger-overlay/binary/upload", (c) =>
45
58
  binaryHttpRouteHandler.postWorkflowDebuggerOverlayBinaryUpload(c.req.raw, {
46
59
  workflowId: c.req.param("workflowId"),
47
60
  }),
48
61
  );
49
- app.notFound((c) => {
62
+ api.notFound((c) => {
50
63
  const method = c.req.method.toUpperCase();
51
64
  const url = new URL(c.req.url);
52
65
  return c.json({ error: `Unknown API route: ${method} ${url.pathname}` }, 404);
53
66
  });
54
- this.app = app;
67
+
68
+ root.route("/", api);
69
+
70
+ // /internal/* routes — only mounted when pairing is configured.
71
+ if (internalRegistrars.length > 0) {
72
+ const internal = new Hono();
73
+ for (const registrar of internalRegistrars) {
74
+ registrar.register(internal);
75
+ }
76
+ root.route("/", internal);
77
+ }
78
+
79
+ this.app = root;
55
80
  }
56
81
 
57
82
  getHono(): Hono {
@@ -0,0 +1,12 @@
1
+ import type { Hono } from "hono";
2
+
3
+ /**
4
+ * Registrar interface for routes mounted on the installation's internal Hono app
5
+ * (no `/api` prefix). All routes registered here are accessible at `/internal/<path>`
6
+ * and are protected by HMAC auth middleware.
7
+ *
8
+ * See docs/pairing-protocol.md for the wire format and auth requirements.
9
+ */
10
+ export interface InternalHonoApiRouteRegistrar {
11
+ register(app: Hono): void;
12
+ }
@@ -0,0 +1,35 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import { Hono } from "hono";
3
+ import type { HonoApiRouteRegistrar } from "../HonoApiRouteRegistrar";
4
+ import type { SessionVerifier } from "../../../../application/auth/SessionVerifier";
5
+ import { ApplicationTokens } from "../../../../applicationTokens";
6
+
7
+ /**
8
+ * Exposes `GET /api/me` in managed-auth mode.
9
+ *
10
+ * Reads the JWT principal by re-verifying the Bearer token, and returns
11
+ * `{ userId, workspaceId }`. No DB lookup needed — the JWT is the source of truth.
12
+ *
13
+ * Only registered when `auth.kind === "managed"`.
14
+ */
15
+ @injectable()
16
+ export class ManagedMeHonoApiRouteRegistrar implements HonoApiRouteRegistrar {
17
+ constructor(
18
+ @inject(ApplicationTokens.SessionVerifier)
19
+ private readonly sessionVerifier: SessionVerifier,
20
+ ) {}
21
+
22
+ register(app: Hono): void {
23
+ app.get("/me", async (c) => {
24
+ try {
25
+ const principal = await this.sessionVerifier.verify(c.req.raw);
26
+ if (!principal) {
27
+ return c.json({ error: "Unauthorized" }, 401);
28
+ }
29
+ return c.json({ userId: principal.id, workspaceId: principal.workspaceId ?? null });
30
+ } catch {
31
+ return c.json({ error: "Unauthorized" }, 401);
32
+ }
33
+ });
34
+ }
35
+ }
@@ -8,8 +8,8 @@ export class OAuth2HonoApiRouteRegistrar implements HonoApiRouteRegistrar {
8
8
  constructor(@inject(OAuth2HttpRouteHandler) private readonly handler: OAuth2HttpRouteHandler) {}
9
9
 
10
10
  register(app: Hono): void {
11
- app.get("/oauth2/auth", (c) => this.handler.getAuthRedirect(c.req.raw));
12
- app.get("/oauth2/callback", (c) => this.handler.getCallback(c.req.raw));
11
+ app.post("/credentials/oauth/start", (c) => this.handler.postOAuthStart(c.req.raw));
12
+ app.get("/oauth2/callback", (c) => this.handler.getOAuthCallback(c.req.raw));
13
13
  app.get("/oauth2/redirect-uri", (c) => this.handler.getRedirectUri(c.req.raw));
14
14
  app.post("/oauth2/disconnect", (c) => this.handler.postDisconnect(c.req.raw));
15
15
  }
@@ -2,6 +2,7 @@ import { inject, injectable } from "@codemation/core";
2
2
  import { HttpRequestJsonBodyReader } from "../HttpRequestJsonBodyReader";
3
3
  import type { CommandBus } from "../../../application/bus/CommandBus";
4
4
  import type { QueryBus } from "../../../application/bus/QueryBus";
5
+ import type { SessionVerifier } from "../../../application/auth/SessionVerifier";
5
6
  import {
6
7
  CreateCredentialInstanceCommand,
7
8
  DeleteCredentialInstanceCommand,
@@ -23,6 +24,8 @@ import {
23
24
  ListCredentialTypesQuery,
24
25
  } from "../../../application/queries/CredentialQueryHandlers";
25
26
  import { ApplicationTokens } from "../../../applicationTokens";
27
+ import type { PairingConfig } from "../../../pairing/pairing.types";
28
+ import { PairingConfigToken } from "../../../pairing/PairingConfigToken";
26
29
  import { ServerHttpErrorResponseFactory } from "../ServerHttpErrorResponseFactory";
27
30
  import type { ServerHttpRouteParams } from "../ServerHttpRouteParams";
28
31
 
@@ -33,6 +36,10 @@ export class CredentialHttpRouteHandler {
33
36
  private readonly queryBus: QueryBus,
34
37
  @inject(ApplicationTokens.CommandBus)
35
38
  private readonly commandBus: CommandBus,
39
+ @inject(ApplicationTokens.SessionVerifier)
40
+ private readonly sessionVerifier: SessionVerifier,
41
+ @inject(PairingConfigToken, { isOptional: true })
42
+ private readonly pairingConfig: PairingConfig | null,
36
43
  ) {}
37
44
 
38
45
  async getCredentialTypes(): Promise<Response> {
@@ -62,6 +69,27 @@ export class CredentialHttpRouteHandler {
62
69
  async getCredentialInstance(request: Request, params: ServerHttpRouteParams): Promise<Response> {
63
70
  try {
64
71
  const withSecrets = new URL(request.url).searchParams.get("withSecrets") === "1";
72
+
73
+ if (withSecrets) {
74
+ // Ownership check: fail-closed.
75
+ // - If the session verifier returns null (unauthenticated), reject.
76
+ // - In managed-JWT mode the principal's workspaceId must match the
77
+ // installation's own workspaceId (from PairingConfig).
78
+ // - In local-auth mode (pairingConfig absent) a valid non-null principal
79
+ // is sufficient — no cross-workspace check is possible or needed.
80
+ const principal = await this.sessionVerifier.verify(request);
81
+ if (!principal) {
82
+ return Response.json({ error: "Forbidden" }, { status: 403 });
83
+ }
84
+ if (
85
+ principal.source === "managed-jwt" &&
86
+ this.pairingConfig !== null &&
87
+ principal.workspaceId !== this.pairingConfig.workspaceId
88
+ ) {
89
+ return Response.json({ error: "Forbidden" }, { status: 403 });
90
+ }
91
+ }
92
+
65
93
  const instance = withSecrets
66
94
  ? await this.queryBus.execute(new GetCredentialInstanceWithSecretsQuery(params.instanceId!))
67
95
  : await this.queryBus.execute(new GetCredentialInstanceQuery(params.instanceId!));
@@ -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 { CredentialInstanceService } from "../../../domain/credentials/CredentialServices";
4
- import { OAuth2ConnectService } from "../../../domain/credentials/OAuth2ConnectServiceFactory";
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(OAuth2ConnectService)
11
- private readonly oauth2ConnectService: OAuth2ConnectService,
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 getAuthRedirect(request: Request): Promise<Response> {
36
+ async getRedirectUri(request: Request): Promise<Response> {
17
37
  try {
18
- const url = new URL(request.url);
19
- const instanceId = url.searchParams.get("instanceId")?.trim();
20
- if (!instanceId) {
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 getCallback(request: Request): Promise<Response> {
46
+ async postDisconnect(request: Request): Promise<Response> {
34
47
  try {
35
48
  const url = new URL(request.url);
36
- const result = await this.oauth2ConnectService.handleCallback({
37
- code: url.searchParams.get("code"),
38
- state: url.searchParams.get("state"),
39
- requestOrigin: this.resolveRequestOrigin(request),
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
- const message = error instanceof Error ? error.message : String(error);
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 getRedirectUri(request: Request): Promise<Response> {
59
+ async postOAuthStart(request: Request): Promise<Response> {
58
60
  try {
59
- return Response.json({
60
- redirectUri: this.oauth2ConnectService.getRedirectUri(this.resolveRequestOrigin(request)),
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 postDisconnect(request: Request): Promise<Response> {
83
+ async getOAuthCallback(request: Request): Promise<Response> {
68
84
  try {
69
85
  const url = new URL(request.url);
70
- const instanceId = url.searchParams.get("instanceId")?.trim();
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.json({ error: "Missing instanceId query parameter." }, { status: 400 });
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
- return Response.json(await this.credentialInstanceService.disconnectOAuth2(instanceId));
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
- return ServerHttpErrorResponseFactory.fromUnknown(error);
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,46 @@ export class CodemationConsumerConfigLoader {
37
38
  private readonly performanceDiagnosticsLogger = new ServerLoggerFactory(
38
39
  logLevelPolicyFactory,
39
40
  ).createPerformanceDiagnostics("codemation-config-loader.timing");
41
+ private readonly bootLogger = new ServerLoggerFactory(logLevelPolicyFactory).create("codemation.boot");
42
+ /**
43
+ * In-flight + completed load promises keyed by `${consumerRoot}|${configPathOverride}`. The
44
+ * boot path constructs MULTIPLE CodemationConsumerConfigLoader instances (one inside the CLI's
45
+ * DatabaseMigrationsApplyService, another inside NextHostEdgeSeedLoader, another inside
46
+ * AppConfigLoader for the disposable runtime) and each independently calls `load(...)`. Without
47
+ * a cache shared across instances, the same `${consumerRoot}` ends up importing
48
+ * codemation.config.ts + discovered workflow modules ~3 times for a single dev boot. The cache
49
+ * has to be static so it spans every loader instance in the process.
50
+ *
51
+ * Callers MUST invoke `invalidateAll()` on a source-change reload — the dev source watcher
52
+ * already tears the runtime down and reboots; it just needs to clear this map first.
53
+ */
54
+ private static readonly resolutionCache = new Map<string, Promise<CodemationConsumerConfigResolution>>();
55
+
56
+ static invalidateAll(): void {
57
+ this.resolutionCache.clear();
58
+ }
40
59
 
41
60
  async load(
42
61
  args: Readonly<{ consumerRoot: string; configPathOverride?: string }>,
62
+ ): Promise<CodemationConsumerConfigResolution> {
63
+ const cacheKey = `${args.consumerRoot}|${args.configPathOverride ?? ""}`;
64
+ const cached = CodemationConsumerConfigLoader.resolutionCache.get(cacheKey);
65
+ if (cached) {
66
+ return cached;
67
+ }
68
+ const promise = this.loadUncached(args);
69
+ CodemationConsumerConfigLoader.resolutionCache.set(cacheKey, promise);
70
+ try {
71
+ return await promise;
72
+ } catch (error) {
73
+ // A failed load shouldn't poison the cache — future retries should re-attempt.
74
+ CodemationConsumerConfigLoader.resolutionCache.delete(cacheKey);
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ private async loadUncached(
80
+ args: Readonly<{ consumerRoot: string; configPathOverride?: string }>,
43
81
  ): Promise<CodemationConsumerConfigResolution> {
44
82
  const loadStarted = performance.now();
45
83
  let mark = loadStarted;
@@ -54,25 +92,36 @@ export class CodemationConsumerConfigLoader {
54
92
  `load.${label} +${delta.toFixed(1)}ms (cumulative ${(now - loadStarted).toFixed(1)}ms)`,
55
93
  );
56
94
  };
57
- const bootstrapSource = await this.resolveConfigPath(args.consumerRoot, args.configPathOverride);
95
+ const bootstrapSource = await BootTimer.measureAsync("config.resolveConfigPath", () =>
96
+ this.resolveConfigPath(args.consumerRoot, args.configPathOverride),
97
+ );
58
98
  phaseMs("resolveConfigPath");
59
99
  if (!bootstrapSource) {
60
100
  throw new Error(
61
101
  'Codemation config not found. Expected "codemation.config.ts" in the consumer project root or "src/".',
62
102
  );
63
103
  }
64
- const moduleExports = await this.importModule(bootstrapSource, importSession);
104
+ const moduleExports = await BootTimer.measureAsync("config.importConfigModule", () =>
105
+ this.importModule(bootstrapSource, importSession),
106
+ );
65
107
  phaseMs("importConfigModule");
66
108
  const rawConfig = this.configExportsResolver.resolveConfig(moduleExports);
67
109
  if (!rawConfig) {
68
110
  throw new Error(`Config file does not export a Codemation config object: ${bootstrapSource}`);
69
111
  }
70
112
  const config = this.configNormalizer.normalize(rawConfig);
71
- const workflowSources = await this.resolveWorkflowSources(args.consumerRoot, config);
113
+ if (rawConfig.codemationVersion) {
114
+ this.bootLogger.info(`codemationVersion: ${rawConfig.codemationVersion}`);
115
+ }
116
+ const workflowSources = await BootTimer.measureAsync("config.resolveWorkflowSources", () =>
117
+ this.resolveWorkflowSources(args.consumerRoot, config),
118
+ );
72
119
  phaseMs("resolveWorkflowSources");
73
- const workflows = this.mergeWorkflows(
74
- config.workflows ?? [],
75
- await this.loadDiscoveredWorkflows(args.consumerRoot, config, workflowSources, importSession),
120
+ const workflows = await BootTimer.measureAsync("config.loadDiscoveredWorkflows", async () =>
121
+ this.mergeWorkflows(
122
+ config.workflows ?? [],
123
+ await this.loadDiscoveredWorkflows(args.consumerRoot, config, workflowSources, importSession),
124
+ ),
76
125
  );
77
126
  phaseMs("loadDiscoveredWorkflows");
78
127
  const resolvedConfig: NormalizedCodemationConfig = {
@@ -145,7 +194,10 @@ export class CodemationConsumerConfigLoader {
145
194
  workflowDiscoveryDirectories,
146
195
  absoluteWorkflowModulePath: workflowSource,
147
196
  }),
148
- moduleExports: await this.importModule(workflowSource, importSession),
197
+ moduleExports: await BootTimer.measureAsync(
198
+ `workflow.${path.basename(workflowSource).replace(/\.tsx?$/, "")}`,
199
+ () => this.importModule(workflowSource, importSession),
200
+ ),
149
201
  })),
150
202
  );
151
203
  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
  );