@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.
Files changed (226) hide show
  1. package/CHANGELOG.md +465 -0
  2. package/LICENSE +1 -37
  3. package/dist/{ApiPaths-CLTHphYZ.js → ApiPaths-Dv1dcHu_.js} +4 -4
  4. package/dist/ApiPaths-Dv1dcHu_.js.map +1 -0
  5. package/dist/{AppConfigFactory-CvpFScwB.js → AppConfigFactory-Cx4qQvRk.js} +114 -53
  6. package/dist/AppConfigFactory-Cx4qQvRk.js.map +1 -0
  7. package/dist/{AppConfigFactory-LK76niPc.d.ts → AppConfigFactory-DnLoQ9Li.d.ts} +8527 -5549
  8. package/dist/{AppContainerFactory-BlLrm6_h.js → AppContainerFactory-DqKYCRNP.js} +7656 -2090
  9. package/dist/AppContainerFactory-DqKYCRNP.js.map +1 -0
  10. package/dist/{CodemationAppContext-CvWi5gey.d.ts → CodemationAppContext-CKVv9W9q.d.ts} +8 -4
  11. package/dist/{CodemationAuthoring.types-BuKNTDC1.d.ts → CodemationAuthoring.types-DA3G3s6d.d.ts} +25 -5
  12. package/dist/{CodemationAuthoring.types-DZl-sJaM.js → CodemationAuthoring.types-NGkBcmmT.js} +18 -6
  13. package/dist/CodemationAuthoring.types-NGkBcmmT.js.map +1 -0
  14. package/dist/{CodemationConfigNormalizer-CYdR0PR5.d.ts → CodemationConfigNormalizer-BAKjetJ6.d.ts} +3 -3
  15. package/dist/{CodemationConsumerConfigLoader-BeAUS144.js → CodemationConsumerConfigLoader-GYpBBvqE.js} +79 -10
  16. package/dist/CodemationConsumerConfigLoader-GYpBBvqE.js.map +1 -0
  17. package/dist/{CodemationConsumerConfigLoader-C3nAj9Bj.d.ts → CodemationConsumerConfigLoader-nxOqvv46.d.ts} +17 -2
  18. package/dist/{CodemationPluginListMerger-B-W5Fa_X.js → CodemationPluginListMerger-D1B1IEbt.js} +1 -1
  19. package/dist/{CodemationPluginListMerger-B-W5Fa_X.js.map → CodemationPluginListMerger-D1B1IEbt.js.map} +1 -1
  20. package/dist/{CodemationPluginListMerger-D-gwVwtw.d.ts → CodemationPluginListMerger-DKLAHT2b.d.ts} +123 -16
  21. package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js +97 -0
  22. package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js.map +1 -0
  23. package/dist/{CodemationWhitelabelConfig-CWbcyQqn.d.ts → CodemationWhitelabelConfig-Ca2mCUeC.d.ts} +2 -2
  24. package/dist/{CollectionContracts.types-DdpHft0i.d.ts → CollectionContracts.types-DDyFYT_D.d.ts} +1 -1
  25. package/dist/{CredentialContractsRegistry-D7mcPed2.d.ts → CredentialContractsRegistry-Bq2bq28t.d.ts} +2 -2
  26. package/dist/{CredentialServices-DdCEP2xt.d.ts → CredentialServices-Be2I60Th.d.ts} +65 -20
  27. package/dist/{CredentialServices-CgxwguAv.js → CredentialServices-Dk8yypeL.js} +310 -51
  28. package/dist/CredentialServices-Dk8yypeL.js.map +1 -0
  29. package/dist/InternalHonoApiRouteRegistrar-Ce1yxpnO.d.ts +17 -0
  30. package/dist/InternalPingRegistrar-DY3kSfxP.js +221 -0
  31. package/dist/InternalPingRegistrar-DY3kSfxP.js.map +1 -0
  32. package/dist/{ItemsInputNormalizer-D1WppVMU.d.ts → ItemsInputNormalizer-_RwIfRIQ.d.ts} +108 -25
  33. package/dist/{LogLevelPolicyFactory-CampWO0l.d.ts → LogLevelPolicyFactory-ewCHLDLn.d.ts} +2 -2
  34. package/dist/{PublicFrontendBootstrap-DzBgwOnG.d.ts → PublicFrontendBootstrap-Cev3qK46.d.ts} +9 -2
  35. package/dist/PublicFrontendBootstrapFactory-CY2FS-5g.d.ts +82 -0
  36. package/dist/{PublicFrontendBootstrapJsonCodec-Cl_DLRh0.d.ts → PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts} +3 -3
  37. package/dist/{PublicFrontendBootstrapJsonCodec-DzqvA0uo.js → PublicFrontendBootstrapJsonCodec-CegIF_ne.js} +7 -2
  38. package/dist/PublicFrontendBootstrapJsonCodec-CegIF_ne.js.map +1 -0
  39. package/dist/ServerLoggerFactory-Ckk52S3w.js +223 -0
  40. package/dist/ServerLoggerFactory-Ckk52S3w.js.map +1 -0
  41. package/dist/{TelemetryContracts-BsOD_Y17.d.ts → TelemetryContracts-BtDx84Cp.d.ts} +13 -4
  42. package/dist/{WorkflowPolicyUiPresentationFactory-DNE5oAI6.d.ts → WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts} +2 -2
  43. package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js} +1 -1
  44. package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js.map → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map} +1 -1
  45. package/dist/{WorkflowViewContracts-0ZgsHQdp.d.ts → WorkflowViewContracts-B7aFQcIw.d.ts} +15 -1
  46. package/dist/authoring.d.ts +5 -5
  47. package/dist/authoring.js +1 -1
  48. package/dist/client.d.ts +4 -4
  49. package/dist/client.js +2 -2
  50. package/dist/consumer.d.ts +6 -6
  51. package/dist/consumer.js +2 -2
  52. package/dist/credentials.d.ts +6 -6
  53. package/dist/credentials.js +1 -1
  54. package/dist/devServerSidecar.d.ts +2 -2
  55. package/dist/devServerSidecar.js +1 -94
  56. package/dist/devServerSidecar.js.map +1 -1
  57. package/dist/dto.d.ts +6 -6
  58. package/dist/{index-BlGs9e9Q.d.ts → index-DilAYwnH.d.ts} +49 -3
  59. package/dist/index.d.ts +110 -21
  60. package/dist/index.js +15 -13
  61. package/dist/mapping.d.ts +2 -2
  62. package/dist/mapping.js +1 -1
  63. package/dist/nextServer.d.ts +43 -88
  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-CpNFYa_q.js → persistenceServer-C-hH4z6l.js} +2 -2
  69. package/dist/{persistenceServer-CpNFYa_q.js.map → persistenceServer-C-hH4z6l.js.map} +1 -1
  70. package/dist/persistenceServer-CeTHtC6E.d.ts +30 -0
  71. package/dist/persistenceServer.d.ts +8 -8
  72. package/dist/persistenceServer.js +3 -3
  73. package/dist/{server-CQWdkT7t.d.ts → server-C4bS62rg.d.ts} +21 -6
  74. package/dist/{server-BK43OKxW.js → server-Y7kxwtCK.js} +7 -6
  75. package/dist/{server-BK43OKxW.js.map → server-Y7kxwtCK.js.map} +1 -1
  76. package/dist/server.d.ts +14 -14
  77. package/dist/server.js +13 -11
  78. package/package.json +29 -42
  79. package/prisma/migrations/20260507120000_execution_instance_child_run_id/migration.sql +5 -0
  80. package/prisma/migrations/20260519000000_workflow_audit_log/migration.sql +23 -0
  81. package/prisma/migrations/20260519100000_storage_growth_fixes/migration.sql +61 -0
  82. package/prisma/migrations.sqlite/20260507120000_execution_instance_child_run_id/migration.sql +5 -0
  83. package/prisma/migrations.sqlite/20260519000000_workflow_audit_log/migration.sql +21 -0
  84. package/prisma/migrations.sqlite/20260519100000_storage_growth_fixes/migration.sql +29 -0
  85. package/prisma/schema.postgresql.prisma +56 -17
  86. package/prisma/schema.sqlite.prisma +56 -17
  87. package/prisma-generated/prisma-postgresql-client/edge.js +35 -6
  88. package/prisma-generated/prisma-postgresql-client/index-browser.js +31 -2
  89. package/prisma-generated/prisma-postgresql-client/index.d.ts +8971 -5718
  90. package/prisma-generated/prisma-postgresql-client/index.js +35 -6
  91. package/prisma-generated/prisma-postgresql-client/package.json +1 -1
  92. package/prisma-generated/prisma-postgresql-client/schema.prisma +39 -0
  93. package/prisma-generated/prisma-sqlite-client/edge.js +35 -6
  94. package/prisma-generated/prisma-sqlite-client/index-browser.js +31 -2
  95. package/prisma-generated/prisma-sqlite-client/index.d.ts +8963 -5715
  96. package/prisma-generated/prisma-sqlite-client/index.js +35 -6
  97. package/prisma-generated/prisma-sqlite-client/package.json +1 -1
  98. package/prisma-generated/prisma-sqlite-client/schema.prisma +39 -0
  99. package/scripts/check-collections.mjs +18 -0
  100. package/scripts/generate-prisma-clients.mjs +20 -11
  101. package/src/application/WorkflowAuditLogPruneScheduler.ts +96 -0
  102. package/src/application/auth/AuthenticatedPrincipal.ts +4 -0
  103. package/src/application/commands/StartWorkflowRunCommandHandler.ts +4 -0
  104. package/src/application/contracts/WorkflowViewContracts.ts +11 -0
  105. package/src/application/contracts/WorkflowWebsocketMessage.ts +3 -1
  106. package/src/application/mapping/WorkflowDefinitionMapper.ts +44 -1
  107. package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +7 -1
  108. package/src/application/telemetry/OtelExecutionTelemetry.types.ts +5 -0
  109. package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +4 -0
  110. package/src/application/telemetry/StoredTelemetrySpanScope.ts +6 -2
  111. package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +27 -17
  112. package/src/application/telemetry/TelemetrySpanPublisher.ts +11 -0
  113. package/src/application/websocket/TelemetrySpanWebsocketRelay.ts +31 -0
  114. package/src/applicationTokens.ts +20 -1
  115. package/src/audit/IAuditEmitter.ts +32 -0
  116. package/src/audit/PrismaWorkflowAuditLogRepository.ts +34 -0
  117. package/src/audit/WorkflowAuditLogWriter.ts +125 -0
  118. package/src/auth/managed/ManagedAuthConfig.ts +29 -0
  119. package/src/auth/managed/ManagedAuthMiddleware.ts +52 -0
  120. package/src/auth/managed/ManagedCorsMiddleware.ts +43 -0
  121. package/src/auth/managed/ManagedModeBootGuard.ts +27 -0
  122. package/src/auth/managed/index.ts +5 -0
  123. package/src/bootstrap/AppContainerFactory.ts +277 -29
  124. package/src/bootstrap/AppContainerLifecycle.ts +31 -0
  125. package/src/bootstrap/perf/BootTimer.ts +168 -0
  126. package/src/bootstrap/runtime/AppConfigFactory.ts +21 -65
  127. package/src/bootstrap/runtime/FrontendRuntime.ts +4 -1
  128. package/src/bootstrap/runtime/WorkerRuntime.ts +2 -1
  129. package/src/credentials/BrokerClient.ts +49 -0
  130. package/src/credentials/BrokerRefreshError.ts +12 -0
  131. package/src/credentials/BrokerRefreshInvalidGrantError.ts +13 -0
  132. package/src/credentials/ControlPlaneCatalogFetcher.ts +261 -0
  133. package/src/credentials/CredentialOAuth2MaterialReader.ts +136 -0
  134. package/src/credentials/InternalCredentialsListRegistrar.ts +48 -0
  135. package/src/credentials/InternalCredentialsPushRegistrar.ts +125 -0
  136. package/src/credentials/LocalOAuthFlowExecutor.ts +316 -0
  137. package/src/credentials/ManagedOAuthFlowExecutor.ts +94 -0
  138. package/src/credentials/ManagedOAuthRefreshInvalidGrantError.ts +13 -0
  139. package/src/credentials/catalogTypes.ts +4 -0
  140. package/src/credentials/refresh/CredentialDisconnectedError.ts +11 -0
  141. package/src/domain/credentials/CredentialBindingService.ts +54 -2
  142. package/src/domain/credentials/CredentialKeyRotatedError.ts +22 -0
  143. package/src/domain/credentials/CredentialSecretCipher.ts +68 -6
  144. package/src/domain/credentials/CredentialTypeRegistryImpl.ts +117 -10
  145. package/src/domain/credentials/OAuth2RedirectUriResolver.ts +79 -0
  146. package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +14 -5
  147. package/src/domain/telemetry/TelemetryContracts.ts +7 -1
  148. package/src/domain/workflows/WorkflowActivationPreflight.ts +24 -1
  149. package/src/domain/workflows/WorkflowActivationPreflightRules.ts +40 -1
  150. package/src/index.ts +6 -0
  151. package/src/infrastructure/binary/LocalFilesystemBinaryStorageRegistry.ts +29 -1
  152. package/src/infrastructure/binary/S3BinaryStorage.ts +169 -0
  153. package/src/infrastructure/binary/S3BinaryStorageConfig.ts +17 -0
  154. package/src/infrastructure/config/CodemationPluginRegistrar.ts +3 -1
  155. package/src/infrastructure/persistence/CodemationDatabaseUrlParser.ts +41 -0
  156. package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +8 -3
  157. package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +1 -0
  158. package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +21 -13
  159. package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +43 -8
  160. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +33 -3
  161. package/src/infrastructure/persistence/PrismaWorkflowSnapshotRepository.ts +48 -0
  162. package/src/mcp/AgentMcpIntegrationImpl.ts +344 -0
  163. package/src/mcp/McpClientFactory.ts +29 -0
  164. package/src/mcp/McpConnectionPool.ts +184 -0
  165. package/src/mcp/McpConnectionPool.types.ts +12 -0
  166. package/src/mcp/McpServerCatalog.ts +104 -0
  167. package/src/mcp/index.ts +5 -0
  168. package/src/pairing/HmacRequestSigner.ts +32 -0
  169. package/src/pairing/IncomingHmacVerifier.ts +82 -0
  170. package/src/pairing/InternalHmacAuthMiddleware.ts +33 -0
  171. package/src/pairing/InternalPingRegistrar.ts +25 -0
  172. package/src/pairing/PairedFetch.ts +33 -0
  173. package/src/pairing/PairingConfigFactory.ts +35 -0
  174. package/src/pairing/PairingConfigToken.ts +6 -0
  175. package/src/pairing/index.ts +14 -0
  176. package/src/pairing/pairing.types.ts +18 -0
  177. package/src/pairing.ts +17 -0
  178. package/src/persistenceServer.ts +1 -0
  179. package/src/presentation/config/AppConfig.ts +7 -1
  180. package/src/presentation/config/CodemationAuthConfig.ts +1 -1
  181. package/src/presentation/config/CodemationAuthoring.types.ts +54 -5
  182. package/src/presentation/config/CodemationConfig.ts +3 -0
  183. package/src/presentation/config/CodemationConfigNormalizer.ts +39 -1
  184. package/src/presentation/config/CodemationPlugin.ts +2 -1
  185. package/src/presentation/frontend/CodemationFrontendAuthSnapshot.ts +5 -0
  186. package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -1
  187. package/src/presentation/frontend/PublicFrontendBootstrap.ts +2 -0
  188. package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +5 -1
  189. package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +4 -1
  190. package/src/presentation/http/ApiPaths.ts +4 -4
  191. package/src/presentation/http/ServerHttpErrorResponseFactory.ts +39 -2
  192. package/src/presentation/http/hono/CodemationHonoApiAppFactory.ts +33 -8
  193. package/src/presentation/http/hono/InternalHonoApiRouteRegistrar.ts +12 -0
  194. package/src/presentation/http/hono/registrars/ManagedMeHonoApiRouteRegistrar.ts +35 -0
  195. package/src/presentation/http/hono/registrars/OAuth2HonoApiRouteRegistrar.ts +2 -2
  196. package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +28 -0
  197. package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +98 -41
  198. package/src/presentation/server/CodemationConsumerConfigLoader.ts +54 -7
  199. package/src/presentation/server/CodemationPluginDiscovery.ts +5 -0
  200. package/src/presentation/server/WorkflowDefinitionExportsResolver.ts +18 -0
  201. package/src/presentation/server/WorkflowModulePathFinder.ts +12 -1
  202. package/src/presentation/websocket/ManagedWebsocketAuthenticator.ts +50 -0
  203. package/src/presentation/websocket/WebsocketAuthenticator.types.ts +12 -0
  204. package/src/presentation/websocket/WorkflowWebsocketServer.ts +24 -3
  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-CvpFScwB.js.map +0 -1
  217. package/dist/AppContainerFactory-BlLrm6_h.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-BMWqNM9a.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-CIVt3UOX.d.ts +0 -9
  226. package/src/domain/credentials/OAuth2ConnectServiceFactory.ts +0 -411
@@ -1,4 +1,4 @@
1
- import type { AnyCredentialType, DefinedCollection, DefinedNode } from "@codemation/core";
1
+ import type { AnyCredentialType, DefinedCollection, DefinedNode, McpServerDeclaration } from "@codemation/core";
2
2
  import type { CodemationAppContext } from "./CodemationAppContext";
3
3
  import type {
4
4
  CodemationAppDefinition,
@@ -12,14 +12,20 @@ import type { CodemationWhitelabelConfig } from "./CodemationWhitelabelConfig";
12
12
  export interface FriendlyCodemationDatabaseConfig {
13
13
  readonly kind: "postgresql" | "sqlite";
14
14
  readonly url?: string;
15
+ /** Name of an environment variable whose value is the PostgreSQL connection URL. Co-exclusive with `url`. */
16
+ readonly urlEnv?: string;
15
17
  readonly filePath?: string;
16
18
  }
17
19
 
18
20
  export interface FriendlyCodemationExecutionConfig {
19
21
  readonly mode?: "inline" | "queue";
22
+ /** Name of an environment variable whose value is "inline" or "queue". Co-exclusive with `mode`. */
23
+ readonly modeEnv?: string;
20
24
  readonly queuePrefix?: string;
21
25
  readonly workerQueues?: ReadonlyArray<string>;
22
26
  readonly redisUrl?: string;
27
+ /** Name of an environment variable whose value is the Redis connection URL. Co-exclusive with `redisUrl`. */
28
+ readonly redisUrlEnv?: string;
23
29
  }
24
30
 
25
31
  export interface DefineCodemationAppOptions extends Omit<
@@ -36,6 +42,13 @@ export interface DefineCodemationAppOptions extends Omit<
36
42
  readonly credentials?: ReadonlyArray<AnyCredentialType>;
37
43
  readonly register?: (context: CodemationAppContext) => void;
38
44
  readonly whitelabel?: CodemationWhitelabelConfig;
45
+ /**
46
+ * Path (relative to the consumer project root) to a directory from which workflows are auto-discovered.
47
+ * All `*.ts` / `*.tsx` files (excluding `*.test.*` and `*.d.ts`) are imported and any exported
48
+ * `WorkflowDefinition` values are registered. Co-exclusive with providing this directory in
49
+ * `workflowDiscovery.directories`.
50
+ */
51
+ readonly workflowsDir?: string;
39
52
  }
40
53
 
41
54
  export interface DefinePluginOptions {
@@ -44,6 +57,7 @@ export interface DefinePluginOptions {
44
57
  readonly nodes?: ReadonlyArray<DefinedNode<string, Record<string, unknown>, unknown, unknown>>;
45
58
  readonly collections?: ReadonlyArray<DefinedCollection>;
46
59
  readonly credentials?: ReadonlyArray<AnyCredentialType>;
60
+ readonly mcpServers?: ReadonlyArray<McpServerDeclaration>;
47
61
  readonly register?: (context: CodemationPluginContext) => void | Promise<void>;
48
62
  readonly sandbox?: CodemationConfig;
49
63
  }
@@ -53,13 +67,15 @@ class CodemationAuthoringConfigFactory {
53
67
  const appDefinition = this.createAppDefinition(options);
54
68
  const credentialTypes = [...(options.credentialTypes ?? []), ...(options.credentials ?? [])];
55
69
  const register = this.composeAppRegister(options.register, options.nodes, options.collections);
56
- const { workflows, workflowDiscovery, plugins, runtime, log } = options;
70
+ const { workflows, plugins, runtime, log, mcpServers } = options;
71
+ const workflowDiscovery = this.mergeWorkflowDiscovery(options.workflowDiscovery, options.workflowsDir);
57
72
  return {
58
73
  workflows,
59
74
  workflowDiscovery,
60
75
  plugins,
61
76
  runtime,
62
77
  log,
78
+ mcpServers,
63
79
  app: appDefinition,
64
80
  credentialTypes,
65
81
  register,
@@ -70,6 +86,7 @@ class CodemationAuthoringConfigFactory {
70
86
  return {
71
87
  pluginPackageId: options.pluginPackageId,
72
88
  sandbox: options.sandbox,
89
+ mcpServers: options.mcpServers,
73
90
  async register(context: CodemationPluginContext): Promise<void> {
74
91
  for (const nodeDefinition of options.nodes ?? []) {
75
92
  nodeDefinition.register(context);
@@ -112,9 +129,15 @@ class CodemationAuthoringConfigFactory {
112
129
  sqliteFilePath: database.filePath,
113
130
  };
114
131
  }
132
+ if (database.url !== undefined && database.urlEnv !== undefined) {
133
+ throw new Error(
134
+ "defineCodemationApp: database.url and database.urlEnv are mutually exclusive — provide one or the other.",
135
+ );
136
+ }
137
+ const url = database.urlEnv !== undefined ? process.env[database.urlEnv] : database.url;
115
138
  return {
116
139
  kind: "postgresql",
117
- url: database.url,
140
+ url,
118
141
  };
119
142
  }
120
143
 
@@ -124,11 +147,37 @@ class CodemationAuthoringConfigFactory {
124
147
  if (!execution) {
125
148
  return undefined;
126
149
  }
150
+ if (execution.mode !== undefined && execution.modeEnv !== undefined) {
151
+ throw new Error(
152
+ "defineCodemationApp: execution.mode and execution.modeEnv are mutually exclusive — provide one or the other.",
153
+ );
154
+ }
155
+ if (execution.redisUrl !== undefined && execution.redisUrlEnv !== undefined) {
156
+ throw new Error(
157
+ "defineCodemationApp: execution.redisUrl and execution.redisUrlEnv are mutually exclusive — provide one or the other.",
158
+ );
159
+ }
160
+ const rawMode = execution.modeEnv !== undefined ? process.env[execution.modeEnv] : execution.mode;
161
+ const mode = rawMode === "inline" || rawMode === "queue" ? rawMode : undefined;
162
+ const redisUrl = execution.redisUrlEnv !== undefined ? process.env[execution.redisUrlEnv] : execution.redisUrl;
127
163
  return {
128
- kind: execution.mode,
164
+ kind: mode,
129
165
  queuePrefix: execution.queuePrefix,
130
166
  workerQueues: execution.workerQueues,
131
- redisUrl: execution.redisUrl,
167
+ redisUrl,
168
+ };
169
+ }
170
+
171
+ private static mergeWorkflowDiscovery(
172
+ existing: CodemationConfig["workflowDiscovery"],
173
+ workflowsDir: string | undefined,
174
+ ): CodemationConfig["workflowDiscovery"] {
175
+ if (!workflowsDir) {
176
+ return existing;
177
+ }
178
+ const existingDirectories = existing?.directories ?? [];
179
+ return {
180
+ directories: [...existingDirectories, workflowsDir],
132
181
  };
133
182
  }
134
183
 
@@ -3,6 +3,7 @@ import type {
3
3
  CollectionDefinition,
4
4
  DefinedCollection,
5
5
  EngineExecutionLimitsPolicyConfig,
6
+ McpServerDeclaration,
6
7
  WorkflowDefinition,
7
8
  } from "@codemation/core";
8
9
  import type { CodemationAuthConfig } from "./CodemationAuthConfig";
@@ -80,6 +81,8 @@ export interface CodemationConfig {
80
81
  readonly plugins?: ReadonlyArray<CodemationPlugin>;
81
82
  /** Consumer-defined `CredentialType` entries (see `@codemation/core`), applied when the host loads config. */
82
83
  readonly credentialTypes?: ReadonlyArray<AnyCredentialType>;
84
+ /** MCP server declarations from this config file (merged with plugin and control-plane sources at startup). */
85
+ readonly mcpServers?: ReadonlyArray<McpServerDeclaration>;
83
86
  /** Declared collections available at runtime via `ctx.collections`. Accepts either raw `CollectionDefinition` or `DefinedCollection` (the result of `defineCollection(...)`). */
84
87
  readonly collections?: ReadonlyArray<CollectionDefinition | DefinedCollection>;
85
88
  /** Optional shell whitelabel (product name, logo path). */
@@ -8,6 +8,7 @@ import type {
8
8
  } from "@codemation/core";
9
9
  import type { CodemationContainerRegistration } from "../../bootstrap/CodemationContainerRegistration";
10
10
  import type { CodemationAppContext } from "./CodemationAppContext";
11
+ import type { CodemationAuthConfig } from "./CodemationAuthConfig";
11
12
  import type { CodemationClassToken } from "./CodemationClassToken";
12
13
  import type {
13
14
  CodemationApplicationRuntimeConfig,
@@ -25,6 +26,9 @@ export type NormalizedCodemationConfig = Omit<CodemationConfig, "collections"> &
25
26
 
26
27
  export class CodemationConfigNormalizer {
27
28
  normalize(config: CodemationConfig): NormalizedCodemationConfig {
29
+ const auth = config.app?.auth ?? config.auth;
30
+ this.assertAuthConfig(auth);
31
+ this.assertManagedModeConstraints(config, auth);
28
32
  const collected = this.collectRegistration(config);
29
33
  const normalizedRuntime = this.normalizeRuntimeConfig(config);
30
34
  const normalizedWorkflowDiscoveryDirectories = [
@@ -34,7 +38,7 @@ export class CodemationConfigNormalizer {
34
38
 
35
39
  return {
36
40
  ...config,
37
- auth: config.app?.auth ?? config.auth,
41
+ auth,
38
42
  containerRegistrations: collected.containerRegistrations,
39
43
  credentialTypes: [...(config.credentialTypes ?? []), ...collected.credentialTypes],
40
44
  collections: [...this.unwrapCollections(config.collections), ...collected.collections],
@@ -49,6 +53,23 @@ export class CodemationConfigNormalizer {
49
53
  };
50
54
  }
51
55
 
56
+ /**
57
+ * Enforces managed-mode invariants beyond what `assertAuthConfig` covers:
58
+ * managed-mode workspaces are always Postgres and always require at least one workflow source.
59
+ */
60
+ private assertManagedModeConstraints(config: CodemationConfig, auth: CodemationAuthConfig | undefined): void {
61
+ if (auth?.kind !== "managed") {
62
+ return;
63
+ }
64
+ const hasWorkflows = (config.workflows?.length ?? 0) > 0;
65
+ const hasWorkflowDiscovery = (config.workflowDiscovery?.directories?.length ?? 0) > 0;
66
+ if (!hasWorkflows && !hasWorkflowDiscovery) {
67
+ throw new Error(
68
+ 'Managed-mode workspaces require at least one workflow source. Provide "workflows" or "workflowsDir" (which maps to workflowDiscovery.directories) in defineCodemationApp.',
69
+ );
70
+ }
71
+ }
72
+
52
73
  private collectRegistration(config: CodemationConfig): Readonly<{
53
74
  containerRegistrations: ReadonlyArray<CodemationContainerRegistration<unknown>>;
54
75
  credentialTypes: ReadonlyArray<AnyCredentialType>;
@@ -187,6 +208,23 @@ export class CodemationConfigNormalizer {
187
208
  };
188
209
  }
189
210
 
211
+ private assertAuthConfig(authConfig: CodemationConfig["auth"]): void {
212
+ if (authConfig?.kind !== "managed") {
213
+ return;
214
+ }
215
+ if (authConfig.oauth && authConfig.oauth.length > 0) {
216
+ throw new Error('auth.kind "managed" cannot be combined with oauth providers. Remove the oauth config.');
217
+ }
218
+ if (authConfig.oidc && authConfig.oidc.length > 0) {
219
+ throw new Error('auth.kind "managed" cannot be combined with oidc providers. Remove the oidc config.');
220
+ }
221
+ if (authConfig.allowUnauthenticatedInDevelopment === true) {
222
+ throw new Error(
223
+ 'auth.kind "managed" cannot be combined with allowUnauthenticatedInDevelopment. Remove that flag.',
224
+ );
225
+ }
226
+ }
227
+
190
228
  private mergeWorkflows(
191
229
  configuredWorkflows: ReadonlyArray<WorkflowDefinition>,
192
230
  registeredWorkflows: ReadonlyArray<WorkflowDefinition>,
@@ -1,4 +1,4 @@
1
- import type { AnyCredentialType, Container } from "@codemation/core";
1
+ import type { AnyCredentialType, Container, McpServerDeclaration } from "@codemation/core";
2
2
  import type { LoggerFactory } from "../../application/logging/Logger";
3
3
  import type { AppConfig } from "./AppConfig";
4
4
  import type { CodemationRegistrationContextBase } from "./CodemationAppContext";
@@ -13,6 +13,7 @@ export interface CodemationPluginContext extends CodemationRegistrationContextBa
13
13
  export interface CodemationPluginConfig {
14
14
  readonly pluginPackageId?: string;
15
15
  readonly credentialTypes?: ReadonlyArray<AnyCredentialType>;
16
+ readonly mcpServers?: ReadonlyArray<McpServerDeclaration>;
16
17
  readonly register?: (context: CodemationPluginContext) => void | Promise<void>;
17
18
  readonly sandbox?: CodemationConfig;
18
19
  }
@@ -11,4 +11,9 @@ export type CodemationFrontendAuthSnapshot = Readonly<{
11
11
  oauthProviders: ReadonlyArray<CodemationFrontendAuthProviderSnapshot>;
12
12
  secret: string | null;
13
13
  uiAuthEnabled: boolean;
14
+ /**
15
+ * Present when auth.kind === "managed". Carries the CP-web origin for CORS
16
+ * awareness. No UI login flow is rendered in managed mode.
17
+ */
18
+ cpWebOrigin?: string;
14
19
  }>;
@@ -29,13 +29,19 @@ export class CodemationFrontendAuthSnapshotFactory {
29
29
  uiAuthEnabled: boolean;
30
30
  }>,
31
31
  ): CodemationFrontendAuthSnapshot {
32
- return {
32
+ const snapshot = {
33
33
  config: args.authConfig,
34
34
  credentialsEnabled: args.authConfig?.kind === "local",
35
35
  oauthProviders: this.createOauthProviders(args.authConfig),
36
36
  secret: this.resolveAuthSecret(args.env),
37
37
  uiAuthEnabled: args.uiAuthEnabled,
38
38
  };
39
+ const cpWebOrigin =
40
+ args.authConfig?.kind === "managed" ? args.env["CP_WEB_ORIGIN"]?.trim() || undefined : undefined;
41
+ if (cpWebOrigin) {
42
+ return { ...snapshot, cpWebOrigin };
43
+ }
44
+ return snapshot;
39
45
  }
40
46
 
41
47
  private resolveUiAuthEnabled(authConfig: CodemationAuthConfig | undefined, env: NodeJS.ProcessEnv): boolean {
@@ -9,4 +9,6 @@ export type PublicFrontendBootstrap = Readonly<{
9
9
  oauthProviders: ReadonlyArray<CodemationFrontendAuthProviderSnapshot>;
10
10
  productName: string;
11
11
  uiAuthEnabled: boolean;
12
+ /** Present in managed mode — the CP web origin. No UI login is rendered when this is set. */
13
+ cpWebOrigin?: string;
12
14
  }>;
@@ -12,12 +12,16 @@ export class PublicFrontendBootstrapFactory {
12
12
 
13
13
  create(): PublicFrontendBootstrap {
14
14
  const frontendAppConfig = this.frontendAppConfigFactory.create();
15
- return {
15
+ const bootstrap: PublicFrontendBootstrap = {
16
16
  credentialsEnabled: frontendAppConfig.auth.credentialsEnabled,
17
17
  logoUrl: frontendAppConfig.logoUrl,
18
18
  oauthProviders: frontendAppConfig.auth.oauthProviders,
19
19
  productName: frontendAppConfig.productName,
20
20
  uiAuthEnabled: frontendAppConfig.auth.uiAuthEnabled,
21
21
  };
22
+ if (frontendAppConfig.auth.cpWebOrigin) {
23
+ return { ...bootstrap, cpWebOrigin: frontendAppConfig.auth.cpWebOrigin };
24
+ }
25
+ return bootstrap;
22
26
  }
23
27
  }
@@ -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
  }
@@ -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!));