@codemation/host 0.8.0 → 0.9.1

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 (148) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/{ApiPaths-Dv1dcHu_.js → ApiPaths-DCvrlIjg.js} +12 -1
  3. package/dist/{ApiPaths-Dv1dcHu_.js.map → ApiPaths-DCvrlIjg.js.map} +1 -1
  4. package/dist/{AppConfigFactory-Cx4qQvRk.js → AppConfigFactory-D4LL1aOR.js} +77 -297
  5. package/dist/AppConfigFactory-D4LL1aOR.js.map +1 -0
  6. package/dist/{AppConfigFactory-BT0y0LVC.d.ts → AppConfigFactory-DncmwCD1.d.ts} +2918 -199
  7. package/dist/{AppContainerFactory-DRTjG7nG.js → AppContainerFactory-CHCXP2rn.js} +1735 -474
  8. package/dist/AppContainerFactory-CHCXP2rn.js.map +1 -0
  9. package/dist/{CodemationAppContext-CGFYVcSb.d.ts → CodemationAppContext-K51b7oXe.d.ts} +3 -3
  10. package/dist/{CodemationAuthoring.types-DiKKogum.d.ts → CodemationAuthoring.types-BXlXIl4K.d.ts} +4 -4
  11. package/dist/{CodemationConfigNormalizer-48f-T66P.d.ts → CodemationConfigNormalizer-B4rDYC9h.d.ts} +3 -3
  12. package/dist/{CodemationConsumerConfigLoader-_PIYqwVx.d.ts → CodemationConsumerConfigLoader-Dt4jyLx6.d.ts} +2 -2
  13. package/dist/{CodemationPluginListMerger-DP7djJ9S.d.ts → CodemationPluginListMerger-DS6I3Xe0.d.ts} +24 -12
  14. package/dist/{persistenceServer-C-hH4z6l.js → CodemationPostgresPrismaClientFactory-C7156Fe-.js} +2 -2
  15. package/dist/CodemationPostgresPrismaClientFactory-C7156Fe-.js.map +1 -0
  16. package/dist/CodemationPostgresPrismaClientFactory-CTNTPnDr.d.ts +9 -0
  17. package/dist/{CredentialContractsRegistry-Bq2bq28t.d.ts → CredentialContractsRegistry-Dgu-rEXi.d.ts} +16 -3
  18. package/dist/{CredentialServices-BLloBztI.d.ts → CredentialServices-B3wPyp2y.d.ts} +4 -4
  19. package/dist/{CredentialServices-Dk8yypeL.js → CredentialServices-Bios0dM8.js} +10 -4
  20. package/dist/CredentialServices-Bios0dM8.js.map +1 -0
  21. package/dist/{InternalHonoApiRouteRegistrar-c7t3KnV_.d.ts → InternalHonoApiRouteRegistrar-Ce1yxpnO.d.ts} +1 -1
  22. package/dist/{InternalPingRegistrar-DY3kSfxP.js → InternalPingRegistrar-BavAAnvk.js} +19 -16
  23. package/dist/InternalPingRegistrar-BavAAnvk.js.map +1 -0
  24. package/dist/{ItemsInputNormalizer-_RwIfRIQ.d.ts → ItemsInputNormalizer-CFkfNMLt.d.ts} +1434 -1225
  25. package/dist/PrismaMigrationDeployer-DdEcXXVi.d.ts +14 -0
  26. package/dist/{PublicFrontendBootstrapFactory-Dv04tJ-6.d.ts → PublicFrontendBootstrapFactory-ClEjZP74.d.ts} +2 -2
  27. package/dist/{PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts → PublicFrontendBootstrapJsonCodec-HNItQ7ol.d.ts} +6 -1
  28. package/dist/{TelemetryContracts-BtDx84Cp.d.ts → TelemetryContracts-DpZEODQM.d.ts} +2 -2
  29. package/dist/{WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts → WorkflowPolicyUiPresentationFactory-BNn2fvR_.d.ts} +2 -2
  30. package/dist/{WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js → WorkflowPolicyUiPresentationFactory-DfvD2VHk.js} +1 -1
  31. package/dist/{WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map → WorkflowPolicyUiPresentationFactory-DfvD2VHk.js.map} +1 -1
  32. package/dist/authoring.d.ts +4 -4
  33. package/dist/client.d.ts +1 -1
  34. package/dist/client.js +1 -1
  35. package/dist/consumer.d.ts +5 -5
  36. package/dist/credentials.d.ts +5 -5
  37. package/dist/credentials.js +1 -1
  38. package/dist/devServerSidecar.d.ts +2 -2
  39. package/dist/dto.d.ts +5 -5
  40. package/dist/{index-DilAYwnH.d.ts → index-ChIfeWzk.d.ts} +71 -28
  41. package/dist/index.d.ts +17 -16
  42. package/dist/index.js +8 -8
  43. package/dist/infrastructure/persistence/PrismaMigrationOperations.d.ts +44 -0
  44. package/dist/infrastructure/persistence/PrismaMigrationOperations.js +302 -0
  45. package/dist/infrastructure/persistence/PrismaMigrationOperations.js.map +1 -0
  46. package/dist/mapping.d.ts +2 -2
  47. package/dist/mapping.js +1 -1
  48. package/dist/nextServer.d.ts +15 -13
  49. package/dist/nextServer.js +6 -6
  50. package/dist/pairing.d.ts +28 -9
  51. package/dist/pairing.js +19 -3
  52. package/dist/pairing.js.map +1 -0
  53. package/dist/{pairing.types-snfZ_OzB.d.ts → pairing.types-D9Bjn98U.d.ts} +1 -1
  54. package/dist/persistenceServer.d.ts +31 -7
  55. package/dist/persistenceServer.js +2 -2
  56. package/dist/{server-09PKasWR.d.ts → server-B5trn7y4.d.ts} +5 -5
  57. package/dist/{server-vtRCPgRJ.js → server-CNj_y0QO.js} +4 -4
  58. package/dist/{server-vtRCPgRJ.js.map → server-CNj_y0QO.js.map} +1 -1
  59. package/dist/server.d.ts +10 -10
  60. package/dist/server.js +8 -8
  61. package/package.json +11 -10
  62. package/playwright.config.ts +8 -2
  63. package/playwright.scaffolded-dev.config.ts +8 -2
  64. package/prisma/migrations/20260526120000_credential_material_pointer/migration.sql +18 -0
  65. package/prisma/migrations/20260527120000_add_human_task/migration.sql +32 -0
  66. package/prisma/migrations/20260527130000_add_hitl_state_json/migration.sql +6 -0
  67. package/prisma/migrations/20260527130000_add_hmac_nonce/migration.sql +12 -0
  68. package/prisma/migrations.sqlite/20260526120000_credential_material_pointer/migration.sql +13 -0
  69. package/prisma/migrations.sqlite/20260527120000_add_human_task/migration.sql +30 -0
  70. package/prisma/migrations.sqlite/20260527130000_add_hitl_state_json/migration.sql +6 -0
  71. package/prisma/migrations.sqlite/20260527130000_add_hmac_nonce/migration.sql +9 -0
  72. package/prisma/schema.postgresql.prisma +48 -0
  73. package/prisma/schema.sqlite.prisma +48 -0
  74. package/prisma-generated/prisma-postgresql-client/edge.js +40 -6
  75. package/prisma-generated/prisma-postgresql-client/index-browser.js +36 -2
  76. package/prisma-generated/prisma-postgresql-client/index.d.ts +3179 -163
  77. package/prisma-generated/prisma-postgresql-client/index.js +40 -6
  78. package/prisma-generated/prisma-postgresql-client/package.json +1 -1
  79. package/prisma-generated/prisma-postgresql-client/schema.prisma +48 -0
  80. package/prisma-generated/prisma-sqlite-client/edge.js +40 -6
  81. package/prisma-generated/prisma-sqlite-client/index-browser.js +36 -2
  82. package/prisma-generated/prisma-sqlite-client/index.d.ts +3175 -163
  83. package/prisma-generated/prisma-sqlite-client/index.js +40 -6
  84. package/prisma-generated/prisma-sqlite-client/package.json +1 -1
  85. package/prisma-generated/prisma-sqlite-client/schema.prisma +48 -0
  86. package/src/application/contracts/CredentialContractsRegistry.ts +15 -0
  87. package/src/application/credentials/AppGalleryProjector.ts +69 -0
  88. package/src/application/hitl/DecideHumanTaskCommandHandler.ts +149 -0
  89. package/src/application/hitl/DecisionSchemaValidator.ts +22 -0
  90. package/src/application/hitl/HitlCallbackHandler.ts +96 -0
  91. package/src/application/mapping/WorkflowDefinitionMapper.ts +1 -3
  92. package/src/application/queries/CredentialQueryHandlers.ts +2 -0
  93. package/src/application/queries/GetCredentialAppsQuery.ts +4 -0
  94. package/src/application/queries/GetCredentialAppsQueryHandler.ts +27 -0
  95. package/src/application/telemetry/ResumeTelemetryContextForRun.ts +53 -0
  96. package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +9 -8
  97. package/src/applicationTokens.ts +11 -1
  98. package/src/auth/managed/ManagedCorsMiddleware.ts +20 -5
  99. package/src/bootstrap/AppContainerFactory.ts +100 -0
  100. package/src/credentials/CachingCredentialMaterialProvider.ts +96 -0
  101. package/src/credentials/CompositeCredentialMaterialProvider.ts +47 -0
  102. package/src/credentials/ControlPlaneCatalogFetcher.ts +4 -24
  103. package/src/credentials/ControlPlaneCredentialMaterialProvider.ts +79 -0
  104. package/src/credentials/CredentialOAuth2MaterialReader.ts +2 -7
  105. package/src/credentials/InternalCredentialsBindingRegistrar.ts +83 -0
  106. package/src/credentials/LocalCredentialMaterialProvider.ts +92 -0
  107. package/src/domain/credentials/CredentialInstanceService.ts +5 -1
  108. package/src/domain/credentials/CredentialTypeRegistryImpl.ts +18 -4
  109. package/src/domain/workflows/WorkflowActivationPreflightRules.ts +7 -4
  110. package/src/dto.ts +2 -0
  111. package/src/hitl/ControlPlaneInboxChannel.ts +102 -0
  112. package/src/hitl/HitlResumeTokenSigner.ts +80 -0
  113. package/src/hitl/HitlTimeoutJobScheduler.ts +89 -0
  114. package/src/hitl/HitlTimeoutWorker.ts +143 -0
  115. package/src/hitl/InboxChannelResolver.ts +49 -0
  116. package/src/hitl/LocalInboxChannel.ts +37 -0
  117. package/src/infrastructure/persistence/PrismaCredentialStore.ts +10 -0
  118. package/src/infrastructure/persistence/PrismaHmacNonceStore.ts +29 -0
  119. package/src/infrastructure/persistence/PrismaHumanTaskStore.ts +156 -0
  120. package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +53 -383
  121. package/src/infrastructure/persistence/PrismaMigrationOperations.ts +401 -0
  122. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +39 -0
  123. package/src/mcp/AgentMcpIntegrationImpl.ts +5 -1
  124. package/src/pairing/HmacNonceStore.ts +14 -0
  125. package/src/pairing/HmacNonceStoreToken.ts +4 -0
  126. package/src/pairing/HmacRequestSigner.ts +10 -1
  127. package/src/pairing/InMemoryHmacNonceStore.ts +24 -0
  128. package/src/pairing/IncomingHmacVerifier.ts +28 -12
  129. package/src/pairing/InternalHmacAuthMiddleware.ts +1 -1
  130. package/src/pairing/index.ts +3 -0
  131. package/src/presentation/http/ApiPaths.ts +14 -0
  132. package/src/presentation/http/hono/HonoHttpAnonymousRoutePolicyRegistry.ts +4 -0
  133. package/src/presentation/http/hono/registrars/CredentialHonoApiRouteRegistrar.ts +1 -0
  134. package/src/presentation/http/hono/registrars/HitlDecideHonoApiRouteRegistrar.ts +54 -0
  135. package/src/presentation/http/hono/registrars/HitlInternalCallbackHonoApiRouteRegistrar.ts +33 -0
  136. package/src/presentation/http/hono/registrars/HitlResumeHonoApiRouteRegistrar.ts +43 -0
  137. package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +9 -0
  138. package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +1 -1
  139. package/src/server.ts +7 -2
  140. package/src/workflows/InternalWorkflowTestRunRegistrar.ts +9 -0
  141. package/tsconfig.json +1 -0
  142. package/dist/AppConfigFactory-Cx4qQvRk.js.map +0 -1
  143. package/dist/AppContainerFactory-DRTjG7nG.js.map +0 -1
  144. package/dist/CredentialServices-Dk8yypeL.js.map +0 -1
  145. package/dist/InternalPingRegistrar-DY3kSfxP.js.map +0 -1
  146. package/dist/persistenceServer-B71RGvSj.d.ts +0 -30
  147. package/dist/persistenceServer-C-hH4z6l.js.map +0 -1
  148. package/src/credentials/catalogTypes.ts +0 -4
@@ -57,6 +57,7 @@ import {
57
57
  UploadOverlayPinnedBinaryCommandHandler,
58
58
  } from "../application/commands/WorkflowCommandHandlers";
59
59
  import {
60
+ GetCredentialAppsQueryHandler,
60
61
  GetCredentialFieldEnvStatusQueryHandler,
61
62
  GetCredentialInstanceQueryHandler,
62
63
  GetCredentialInstanceWithSecretsQueryHandler,
@@ -64,6 +65,7 @@ import {
64
65
  ListCredentialInstancesQueryHandler,
65
66
  ListCredentialTypesQueryHandler,
66
67
  } from "../application/queries/CredentialQueryHandlers";
68
+ import { AppGalleryProjector } from "../application/credentials/AppGalleryProjector";
67
69
  import {
68
70
  ListUserAccountsQueryHandler,
69
71
  VerifyUserInviteQueryHandler,
@@ -278,11 +280,18 @@ import { HmacRequestSigner } from "../pairing/HmacRequestSigner";
278
280
  import { PairedFetch } from "../pairing/PairedFetch";
279
281
  import { IncomingHmacVerifier } from "../pairing/IncomingHmacVerifier";
280
282
  import { InternalHmacAuthMiddleware } from "../pairing/InternalHmacAuthMiddleware";
283
+ import { HmacNonceStoreToken } from "../pairing/HmacNonceStoreToken";
284
+ import { PrismaHmacNonceStore } from "../infrastructure/persistence/PrismaHmacNonceStore";
281
285
  import { InternalPingRegistrar } from "../pairing/InternalPingRegistrar";
282
286
  import { LocalOAuthFlowExecutor } from "../credentials/LocalOAuthFlowExecutor";
287
+ import { LocalCredentialMaterialProvider } from "../credentials/LocalCredentialMaterialProvider";
288
+ import { CachingCredentialMaterialProvider } from "../credentials/CachingCredentialMaterialProvider";
289
+ import { ControlPlaneCredentialMaterialProvider } from "../credentials/ControlPlaneCredentialMaterialProvider";
290
+ import { CompositeCredentialMaterialProvider } from "../credentials/CompositeCredentialMaterialProvider";
283
291
  import { CredentialOAuth2MaterialReader } from "../credentials/CredentialOAuth2MaterialReader";
284
292
  import { ManagedOAuthFlowExecutor } from "../credentials/ManagedOAuthFlowExecutor";
285
293
  import { BrokerClient } from "../credentials/BrokerClient";
294
+ import { InternalCredentialsBindingRegistrar } from "../credentials/InternalCredentialsBindingRegistrar";
286
295
  import { InternalCredentialsPushRegistrar } from "../credentials/InternalCredentialsPushRegistrar";
287
296
  import { InternalCredentialsListRegistrar } from "../credentials/InternalCredentialsListRegistrar";
288
297
  import { InternalWorkflowsListRegistrar } from "../workflows/InternalWorkflowsListRegistrar";
@@ -301,6 +310,23 @@ import { ManagedWebsocketAuthenticator } from "../presentation/websocket/Managed
301
310
  import { JwksCache, ManagedJwtVerifier } from "@codemation/managed-auth";
302
311
  import { CodemationTsyringeTypeInfoRegistrar } from "../presentation/server/CodemationTsyringeTypeInfoRegistrar";
303
312
  import { ControlPlaneCatalogFetcher } from "../credentials/ControlPlaneCatalogFetcher";
313
+ import { PrismaHumanTaskStore } from "../infrastructure/persistence/PrismaHumanTaskStore";
314
+ import { HitlResumeTokenSigner } from "../hitl/HitlResumeTokenSigner";
315
+ import { HitlTimeoutJobScheduler } from "../hitl/HitlTimeoutJobScheduler";
316
+ import { HitlTimeoutWorker } from "../hitl/HitlTimeoutWorker";
317
+ import { DecideHumanTaskCommandHandler } from "../application/hitl/DecideHumanTaskCommandHandler";
318
+ import { DecisionSchemaValidator } from "../application/hitl/DecisionSchemaValidator";
319
+ import { HitlDecideHonoApiRouteRegistrar } from "../presentation/http/hono/registrars/HitlDecideHonoApiRouteRegistrar";
320
+ import { HitlResumeHonoApiRouteRegistrar } from "../presentation/http/hono/registrars/HitlResumeHonoApiRouteRegistrar";
321
+ import { HumanTaskStoreToken } from "@codemation/core";
322
+ import { HitlResumeTokenSignerToken, HitlTimeoutJobSchedulerToken, HitlWorkspaceIdToken } from "@codemation/core";
323
+ import { ControlPlaneInboxChannelToken, InboxChannelResolverToken, LocalInboxChannelToken } from "@codemation/core";
324
+ import { InboxChannelResolver } from "../hitl/InboxChannelResolver";
325
+ import { LocalInboxChannel } from "../hitl/LocalInboxChannel";
326
+ import { ControlPlaneInboxChannel } from "../hitl/ControlPlaneInboxChannel";
327
+ import { HitlCallbackHandler } from "../application/hitl/HitlCallbackHandler";
328
+ import { HitlInternalCallbackHonoApiRouteRegistrar } from "../presentation/http/hono/registrars/HitlInternalCallbackHonoApiRouteRegistrar";
329
+ import { ResumeTelemetryContextForRun } from "../application/telemetry/ResumeTelemetryContextForRun";
304
330
 
305
331
  type AppContainerInputs = Readonly<{
306
332
  appConfig: AppConfig;
@@ -313,6 +339,7 @@ type PrismaOwnership = Readonly<{
313
339
 
314
340
  export class AppContainerFactory {
315
341
  private static readonly queryHandlers = [
342
+ GetCredentialAppsQueryHandler,
316
343
  GetCredentialFieldEnvStatusQueryHandler,
317
344
  GetCredentialInstanceQueryHandler,
318
345
  GetCredentialInstanceWithSecretsQueryHandler,
@@ -380,6 +407,8 @@ export class AppContainerFactory {
380
407
  WhitelabelHonoApiRouteRegistrar,
381
408
  WorkflowHonoApiRouteRegistrar,
382
409
  CollectionHonoApiRouteRegistrar,
410
+ HitlDecideHonoApiRouteRegistrar,
411
+ HitlResumeHonoApiRouteRegistrar,
383
412
  ] as const;
384
413
 
385
414
  constructor(
@@ -718,6 +747,7 @@ export class AppContainerFactory {
718
747
  container.registerSingleton(CredentialRuntimeMaterialService, CredentialRuntimeMaterialService);
719
748
  container.registerSingleton(WorkflowCredentialNodeResolver, WorkflowCredentialNodeResolver);
720
749
  container.registerSingleton(CredentialInstanceService, CredentialInstanceService);
750
+ container.registerSingleton(AppGalleryProjector, AppGalleryProjector);
721
751
  container.registerSingleton(CredentialBindingService, CredentialBindingService);
722
752
  container.registerSingleton(WorkflowActivationPreflightRules, WorkflowActivationPreflightRules);
723
753
  container.registerSingleton(WorkflowActivationPreflight, WorkflowActivationPreflight);
@@ -740,6 +770,26 @@ export class AppContainerFactory {
740
770
  container.registerSingleton(LocalOAuthFlowExecutor, LocalOAuthFlowExecutor);
741
771
  }
742
772
  container.registerSingleton(CredentialOAuth2MaterialReader, CredentialOAuth2MaterialReader);
773
+ // Register the local material provider unconditionally. The dispatcher picks
774
+ // between this and the control-plane provider in managed mode.
775
+ container.registerSingleton(LocalCredentialMaterialProvider, LocalCredentialMaterialProvider);
776
+ // In managed mode (paired with a
777
+ // control plane) wrap the cache around a `CompositeCredentialMaterialProvider`
778
+ // that dispatches by `ref.source`. In standalone mode the cache wraps the
779
+ // local provider directly.
780
+ if (container.isRegistered(PairedFetch, true)) {
781
+ container.registerSingleton(ControlPlaneCredentialMaterialProvider, ControlPlaneCredentialMaterialProvider);
782
+ container.registerSingleton(CompositeCredentialMaterialProvider, CompositeCredentialMaterialProvider);
783
+ container.register(ApplicationTokens.CredentialMaterialInnerProvider, {
784
+ useFactory: instanceCachingFactory((c) => c.resolve(CompositeCredentialMaterialProvider)),
785
+ });
786
+ } else {
787
+ container.register(ApplicationTokens.CredentialMaterialInnerProvider, {
788
+ useFactory: instanceCachingFactory((c) => c.resolve(LocalCredentialMaterialProvider)),
789
+ });
790
+ }
791
+ // In-memory TTL cache decorator.
792
+ container.registerSingleton(CachingCredentialMaterialProvider, CachingCredentialMaterialProvider);
743
793
  container.registerSingleton(CodemationFrontendAuthSnapshotFactory, CodemationFrontendAuthSnapshotFactory);
744
794
  container.registerSingleton(FrontendAppConfigFactory, FrontendAppConfigFactory);
745
795
  container.registerSingleton(PublicFrontendBootstrapFactory, PublicFrontendBootstrapFactory);
@@ -852,6 +902,7 @@ export class AppContainerFactory {
852
902
  ),
853
903
  });
854
904
  container.registerSingleton(OtelExecutionTelemetryFactory, OtelExecutionTelemetryFactory);
905
+ container.registerSingleton(ResumeTelemetryContextForRun, ResumeTelemetryContextForRun);
855
906
  container.registerSingleton(InMemoryRunTraceContextRepository, InMemoryRunTraceContextRepository);
856
907
  container.registerSingleton(InMemoryTelemetrySpanStore, InMemoryTelemetrySpanStore);
857
908
  container.registerSingleton(InMemoryTelemetryArtifactStore, InMemoryTelemetryArtifactStore);
@@ -874,6 +925,32 @@ export class AppContainerFactory {
874
925
  container.registerSingleton(PrismaWorkflowActivationRepository, PrismaWorkflowActivationRepository);
875
926
  container.registerSingleton(InMemoryWorkflowActivationRepository, InMemoryWorkflowActivationRepository);
876
927
  container.registerSingleton(PrismaCredentialStore, PrismaCredentialStore);
928
+ // HITL: token signer + timeout scheduler (persistence wired in registerRuntimeInfrastructure)
929
+ container.registerSingleton(PrismaHumanTaskStore, PrismaHumanTaskStore);
930
+ // Default: no HITL store; overridden to PrismaHumanTaskStore in the Prisma path below
931
+ container.register(HumanTaskStoreToken, { useFactory: () => undefined });
932
+ container.registerSingleton(HitlResumeTokenSigner, HitlResumeTokenSigner);
933
+ container.register(HitlResumeTokenSignerToken, {
934
+ useFactory: instanceCachingFactory((dc) => dc.resolve(HitlResumeTokenSigner)),
935
+ });
936
+ container.registerSingleton(HitlTimeoutJobScheduler, HitlTimeoutJobScheduler);
937
+ container.register(HitlTimeoutJobSchedulerToken, {
938
+ useFactory: instanceCachingFactory((dc) => dc.resolve(HitlTimeoutJobScheduler)),
939
+ });
940
+ container.registerSingleton(HitlTimeoutWorker, HitlTimeoutWorker);
941
+ container.registerSingleton(DecisionSchemaValidator, DecisionSchemaValidator);
942
+ container.registerSingleton(DecideHumanTaskCommandHandler, DecideHumanTaskCommandHandler);
943
+ // HITL: inbox channel resolver (concrete local + control-plane channels registered below)
944
+ container.registerSingleton(InboxChannelResolver, InboxChannelResolver);
945
+ container.register(InboxChannelResolverToken, {
946
+ useFactory: instanceCachingFactory((dc) => dc.resolve(InboxChannelResolver)),
947
+ });
948
+ // HITL: local inbox channel — registered unconditionally; the resolver picks it
949
+ // whenever managed-mode CP channel is not present.
950
+ container.registerSingleton(LocalInboxChannel, LocalInboxChannel);
951
+ container.register(LocalInboxChannelToken, {
952
+ useFactory: instanceCachingFactory((dc) => dc.resolve(LocalInboxChannel)),
953
+ });
877
954
  container.register(ApplicationTokens.WorkflowDefinitionRepository, {
878
955
  useFactory: instanceCachingFactory(
879
956
  (dependencyContainer) =>
@@ -1024,18 +1101,33 @@ export class AppContainerFactory {
1024
1101
  return;
1025
1102
  }
1026
1103
  container.registerInstance(PairingConfigToken, pairingConfig);
1104
+ // T7: Stamp workspaceId on HumanTaskRecord in managed mode for defense-in-depth workspace check.
1105
+ container.registerInstance(HitlWorkspaceIdToken, pairingConfig.workspaceId);
1027
1106
  container.registerSingleton(HmacRequestSigner, HmacRequestSigner);
1028
1107
  container.registerSingleton(PairedFetch, PairedFetch);
1108
+ // T6: Durable nonce store — PrismaHmacNonceStore survives process restarts.
1109
+ container.registerSingleton(HmacNonceStoreToken, PrismaHmacNonceStore);
1029
1110
  container.registerSingleton(IncomingHmacVerifier, IncomingHmacVerifier);
1030
1111
  container.registerSingleton(InternalHmacAuthMiddleware, InternalHmacAuthMiddleware);
1031
1112
  container.registerSingleton(BrokerClient, BrokerClient);
1032
1113
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalPingRegistrar);
1033
1114
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalCredentialsPushRegistrar);
1115
+ container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalCredentialsBindingRegistrar);
1034
1116
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalCredentialsListRegistrar);
1035
1117
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalWorkflowsListRegistrar);
1036
1118
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalWorkflowDetailRegistrar);
1037
1119
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalWorkflowActivationRegistrar);
1038
1120
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalWorkflowTestRunRegistrar);
1121
+ // HITL: CP inbox channel + inbound decision callback (managed mode only)
1122
+ container.registerSingleton(ControlPlaneInboxChannel, ControlPlaneInboxChannel);
1123
+ container.register(ControlPlaneInboxChannelToken, {
1124
+ useFactory: instanceCachingFactory((dc) => dc.resolve(ControlPlaneInboxChannel)),
1125
+ });
1126
+ container.registerSingleton(HitlCallbackHandler, HitlCallbackHandler);
1127
+ container.registerSingleton(
1128
+ ApplicationTokens.InternalHonoApiRouteRegistrar,
1129
+ HitlInternalCallbackHonoApiRouteRegistrar,
1130
+ );
1039
1131
  }
1040
1132
 
1041
1133
  private registerOperationalInfrastructure(container: Container): void {
@@ -1122,6 +1214,9 @@ export class AppContainerFactory {
1122
1214
  binaryStorage,
1123
1215
  new LazyExecutionTelemetryFactory(() => container.resolve(OtelExecutionTelemetryFactory)),
1124
1216
  new CatalogBackedCostTrackingTelemetryFactory(new StaticCostCatalog(FrameworkCostCatalogEntries)),
1217
+ undefined,
1218
+ undefined,
1219
+ container,
1125
1220
  ),
1126
1221
  );
1127
1222
  this.registerRuntimeNodeActivationScheduler(container);
@@ -1159,6 +1254,8 @@ export class AppContainerFactory {
1159
1254
  container.resolve(PrismaWorkflowActivationRepository),
1160
1255
  );
1161
1256
  container.registerInstance(ApplicationTokens.CredentialStore, container.resolve(PrismaCredentialStore));
1257
+ // HITL: wire PrismaHumanTaskStore now that PrismaDatabaseClientToken is available
1258
+ container.registerInstance(HumanTaskStoreToken, container.resolve(PrismaHumanTaskStore));
1162
1259
  container.registerInstance(ApplicationTokens.RunTraceContextRepository, runTraceContextRepository);
1163
1260
  container.registerInstance(ApplicationTokens.TelemetrySpanStore, telemetrySpanStore);
1164
1261
  container.registerInstance(ApplicationTokens.TelemetryArtifactStore, telemetryArtifactStore);
@@ -1171,6 +1268,9 @@ export class AppContainerFactory {
1171
1268
  binaryStorage,
1172
1269
  new LazyExecutionTelemetryFactory(() => container.resolve(OtelExecutionTelemetryFactory)),
1173
1270
  new CatalogBackedCostTrackingTelemetryFactory(new StaticCostCatalog(FrameworkCostCatalogEntries)),
1271
+ undefined,
1272
+ undefined,
1273
+ container,
1174
1274
  ),
1175
1275
  );
1176
1276
  if (appConfig.scheduler.kind === "bullmq") {
@@ -0,0 +1,96 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import type {
3
+ CallerContext,
4
+ CredentialMaterialProvider,
5
+ CredentialMaterialRef,
6
+ MaterialBundle,
7
+ } from "@codemation/core";
8
+
9
+ import { ApplicationTokens } from "../applicationTokens";
10
+ import type { Logger, LoggerFactory } from "../application/logging/Logger";
11
+
12
+ type CacheEntry = Readonly<{
13
+ material: MaterialBundle;
14
+ expiresAt: number;
15
+ }>;
16
+
17
+ /**
18
+ * In-memory TTL cache decorator for `CredentialMaterialProvider`.
19
+ *
20
+ * - Hits avoid the wrapped provider (no CP RPC, no audit row).
21
+ * - Misses/expired entries delegate to the wrapped provider and store the
22
+ * result with TTL = `min(material.expiresAt − 60s, now + 5min)`.
23
+ * - `setMaterial` delegates and then invalidates the entry, so the next
24
+ * `getMaterial` re-fetches fresh bytes.
25
+ *
26
+ * Cache is process-local only — never serialized, never shared across pods.
27
+ * See `docs/design/credentials-oauth-unification.md` and
28
+ * `planning/sprints/credentials-vault/03-in-memory-material-cache.md`.
29
+ */
30
+ @injectable()
31
+ export class CachingCredentialMaterialProvider implements CredentialMaterialProvider {
32
+ private static readonly HARD_CAP_MS = 5 * 60 * 1000;
33
+ private static readonly EXPIRY_SAFETY_WINDOW_MS = 60 * 1000;
34
+
35
+ private readonly cache = new Map<string, CacheEntry>();
36
+ private readonly logger: Logger;
37
+
38
+ constructor(
39
+ @inject(ApplicationTokens.CredentialMaterialInnerProvider) private readonly inner: CredentialMaterialProvider,
40
+ @inject(ApplicationTokens.LoggerFactory) loggerFactory: LoggerFactory,
41
+ ) {
42
+ this.logger = loggerFactory.create("codemation.credentials.material-cache");
43
+ }
44
+
45
+ async getMaterial(ref: CredentialMaterialRef, context: CallerContext): Promise<MaterialBundle> {
46
+ const key = this.keyFor(ref);
47
+ const now = Date.now();
48
+ const entry = this.cache.get(key);
49
+ if (entry && entry.expiresAt > now) {
50
+ this.logger.debug(`material-cache hit key=${key}`);
51
+ return entry.material;
52
+ }
53
+ if (entry) {
54
+ this.logger.debug(`material-cache expired key=${key}`);
55
+ this.cache.delete(key);
56
+ } else {
57
+ this.logger.debug(`material-cache miss key=${key}`);
58
+ }
59
+ const material = await this.inner.getMaterial(ref, context);
60
+ const ttlExpiry = this.computeCacheExpiry(material, Date.now());
61
+ if (ttlExpiry !== null) {
62
+ this.cache.set(key, { material, expiresAt: ttlExpiry });
63
+ }
64
+ return material;
65
+ }
66
+
67
+ async setMaterial(ref: CredentialMaterialRef, material: MaterialBundle): Promise<void> {
68
+ await this.inner.setMaterial(ref, material);
69
+ this.cache.delete(this.keyFor(ref));
70
+ }
71
+
72
+ private keyFor(ref: CredentialMaterialRef): string {
73
+ return `${ref.source}::${ref.id}`;
74
+ }
75
+
76
+ /**
77
+ * Returns the absolute epoch-ms at which the cache entry should expire, or
78
+ * `null` if the entry should not be cached (computed TTL ≤ 0).
79
+ */
80
+ private computeCacheExpiry(material: MaterialBundle, now: number): number | null {
81
+ const hardCapExpiry = now + CachingCredentialMaterialProvider.HARD_CAP_MS;
82
+ if (material.expiresAt === undefined) {
83
+ return hardCapExpiry;
84
+ }
85
+ const parsed = Date.parse(material.expiresAt);
86
+ if (Number.isNaN(parsed)) {
87
+ return hardCapExpiry;
88
+ }
89
+ const safeExpiry = parsed - CachingCredentialMaterialProvider.EXPIRY_SAFETY_WINDOW_MS;
90
+ const clamped = Math.min(safeExpiry, hardCapExpiry);
91
+ if (clamped <= now) {
92
+ return null;
93
+ }
94
+ return clamped;
95
+ }
96
+ }
@@ -0,0 +1,47 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import type {
3
+ CallerContext,
4
+ CredentialMaterialProvider,
5
+ CredentialMaterialRef,
6
+ MaterialBundle,
7
+ } from "@codemation/core";
8
+ import { ManagedCredentialMaterialWriteError } from "@codemation/core";
9
+
10
+ import { LocalCredentialMaterialProvider } from "./LocalCredentialMaterialProvider";
11
+ import { ControlPlaneCredentialMaterialProvider } from "./ControlPlaneCredentialMaterialProvider";
12
+
13
+ /**
14
+ * Routes `getMaterial` / `setMaterial` to the right inner provider based on
15
+ * `ref.source`. Registered as the inner of `CachingCredentialMaterialProvider`
16
+ * in managed mode so workspaces with mixed local + control-plane credential
17
+ * instances read each through the correct provider.
18
+ *
19
+ * Writes against `source: "control-plane"` always throw
20
+ * `ManagedCredentialMaterialWriteError` — managed credential bytes are owned
21
+ * by the control plane.
22
+ *
23
+ * See `planning/sprints/credentials-vault/02-controlplane-material-provider.md`.
24
+ */
25
+ @injectable()
26
+ export class CompositeCredentialMaterialProvider implements CredentialMaterialProvider {
27
+ constructor(
28
+ @inject(LocalCredentialMaterialProvider) private readonly local: LocalCredentialMaterialProvider,
29
+ @inject(ControlPlaneCredentialMaterialProvider)
30
+ private readonly controlPlane: ControlPlaneCredentialMaterialProvider,
31
+ ) {}
32
+
33
+ async getMaterial(ref: CredentialMaterialRef, context: CallerContext): Promise<MaterialBundle> {
34
+ return this.pick(ref).getMaterial(ref, context);
35
+ }
36
+
37
+ async setMaterial(ref: CredentialMaterialRef, material: MaterialBundle): Promise<void> {
38
+ if (ref.source === "control-plane") {
39
+ throw new ManagedCredentialMaterialWriteError();
40
+ }
41
+ return this.pick(ref).setMaterial(ref, material);
42
+ }
43
+
44
+ private pick(ref: CredentialMaterialRef): CredentialMaterialProvider {
45
+ return ref.source === "control-plane" ? this.controlPlane : this.local;
46
+ }
47
+ }
@@ -6,7 +6,6 @@ import type { AppConfig } from "../presentation/config/AppConfig";
6
6
  import { PairedFetch } from "../pairing/PairedFetch";
7
7
  import { PairingConfigToken } from "../pairing/PairingConfigToken";
8
8
  import type { PairingConfig } from "../pairing/pairing.types";
9
- import type { OAuthAppCatalogEntry } from "./catalogTypes";
10
9
 
11
10
  /**
12
11
  * Configuration read from env at construction time.
@@ -27,12 +26,11 @@ type EndpointState = {
27
26
  };
28
27
 
29
28
  /**
30
- * Polls the three control-plane catalog endpoints on a configurable interval,
29
+ * Polls the control-plane catalog endpoints on a configurable interval,
31
30
  * caches the last-known-good responses, and exposes the fetched data for
32
- * credential-type overrides, MCP server registrations, and OAuth app availability.
31
+ * credential-type overrides and MCP server registrations.
33
32
  *
34
33
  * Endpoints (HMAC-gated via PairedFetch):
35
- * GET /internal/catalog/oauth-apps
36
34
  * GET /internal/catalog/mcp-servers
37
35
  * GET /internal/catalog/credential-types
38
36
  *
@@ -41,7 +39,7 @@ type EndpointState = {
41
39
  * are tracked independently.
42
40
  *
43
41
  * When not paired with a control plane (PairingConfigToken is null),
44
- * start() returns immediately and all three getters remain null.
42
+ * start() returns immediately and all getters remain null.
45
43
  */
46
44
  @injectable()
47
45
  export class ControlPlaneCatalogFetcher {
@@ -51,11 +49,9 @@ export class ControlPlaneCatalogFetcher {
51
49
  /** Tracks in-flight refresh so stop() can safely await it. */
52
50
  private inFlight: Promise<void> | null = null;
53
51
 
54
- private _oauthApps: readonly OAuthAppCatalogEntry[] | null = null;
55
52
  private _mcpServers: readonly McpServerDeclaration[] | null = null;
56
53
  private _credentialTypeOverrides: readonly CredentialTypeDefinition[] | null = null;
57
54
 
58
- private readonly oauthAppsState: EndpointState = { consecutiveFailures: 0, lastSuccessAt: null };
59
55
  private readonly mcpServersState: EndpointState = { consecutiveFailures: 0, lastSuccessAt: null };
60
56
  private readonly credentialTypesState: EndpointState = { consecutiveFailures: 0, lastSuccessAt: null };
61
57
 
@@ -82,11 +78,6 @@ export class ControlPlaneCatalogFetcher {
82
78
  };
83
79
  }
84
80
 
85
- /** Latest fetched OAuth app catalog; null until first successful fetch. */
86
- get oauthApps(): readonly OAuthAppCatalogEntry[] | null {
87
- return this._oauthApps;
88
- }
89
-
90
81
  /** Latest fetched MCP server declarations; null until first successful fetch. */
91
82
  get mcpServers(): readonly McpServerDeclaration[] | null {
92
83
  return this._mcpServers;
@@ -163,22 +154,11 @@ export class ControlPlaneCatalogFetcher {
163
154
  const logger = this.loggers.create("ControlPlaneCatalogFetcher");
164
155
  const base = this.pairingConfig.controlPlaneUrl;
165
156
 
166
- const [oauthResult, mcpResult, credTypesResult] = await Promise.allSettled([
167
- this.pairedFetch.get(`${base}/internal/catalog/oauth-apps`),
157
+ const [mcpResult, credTypesResult] = await Promise.allSettled([
168
158
  this.pairedFetch.get(`${base}/internal/catalog/mcp-servers`),
169
159
  this.pairedFetch.get(`${base}/internal/catalog/credential-types`),
170
160
  ]);
171
161
 
172
- await this.handleEndpointResult(
173
- oauthResult,
174
- this.oauthAppsState,
175
- "oauth-apps",
176
- (data) => {
177
- this._oauthApps = data as OAuthAppCatalogEntry[];
178
- },
179
- logger,
180
- );
181
-
182
162
  await this.handleEndpointResult(
183
163
  mcpResult,
184
164
  this.mcpServersState,
@@ -0,0 +1,79 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import type {
3
+ CallerContext,
4
+ CredentialMaterialProvider,
5
+ CredentialMaterialRef,
6
+ MaterialBundle,
7
+ } from "@codemation/core";
8
+ import {
9
+ IllegalMaterialSourceError,
10
+ ManagedCredentialMaterialWriteError,
11
+ ManagedMaterialFetchError,
12
+ } from "@codemation/core";
13
+
14
+ import { PairedFetch } from "../pairing/PairedFetch";
15
+ import { PairingConfigToken } from "../pairing/PairingConfigToken";
16
+ import type { PairingConfig } from "../pairing/pairing.types";
17
+
18
+ /**
19
+ * Control-plane (managed-mode) implementation of `CredentialMaterialProvider`.
20
+ *
21
+ * `getMaterial({ source: "control-plane", id }, callerContext)` HMAC-POSTs to
22
+ * `<CP>/internal/credentials/material/:id`
23
+ * with body `{ callerContext }`. The CP endpoint refreshes upstream tokens as
24
+ * needed and returns `{ accessToken, expiresAt, scopes, providerAccountId, typeId }`.
25
+ * The refresh token never crosses this boundary.
26
+ *
27
+ * `getMaterial` for `source: "local"` throws `IllegalMaterialSourceError`; a
28
+ * dispatcher (`CompositeCredentialMaterialProvider`) routes by source.
29
+ *
30
+ * `setMaterial` always throws `ManagedCredentialMaterialWriteError` — managed
31
+ * credential bytes are owned by the control plane.
32
+ *
33
+ * See `docs/design/credentials-oauth-unification.md` and
34
+ * `planning/sprints/credentials-vault/02-controlplane-material-provider.md`.
35
+ */
36
+ @injectable()
37
+ export class ControlPlaneCredentialMaterialProvider implements CredentialMaterialProvider {
38
+ constructor(
39
+ @inject(PairedFetch) private readonly pairedFetch: PairedFetch,
40
+ @inject(PairingConfigToken) private readonly pairingConfig: PairingConfig,
41
+ ) {}
42
+
43
+ async getMaterial(ref: CredentialMaterialRef, context: CallerContext): Promise<MaterialBundle> {
44
+ if (ref.source !== "control-plane") {
45
+ throw new IllegalMaterialSourceError(ref.source, "ControlPlaneCredentialMaterialProvider");
46
+ }
47
+ const url = `${this.pairingConfig.controlPlaneUrl}/internal/credentials/material/${encodeURIComponent(ref.id)}`;
48
+ const response = await this.pairedFetch.post(url, { callerContext: context });
49
+ if (!response.ok) {
50
+ const body = await response.text().catch(() => "");
51
+ throw new ManagedMaterialFetchError(response.status, body.slice(0, 500));
52
+ }
53
+ const json = (await response.json()) as {
54
+ accessToken?: unknown;
55
+ expiresAt?: unknown;
56
+ scopes?: unknown;
57
+ providerAccountId?: unknown;
58
+ typeId?: unknown;
59
+ };
60
+ if (typeof json.accessToken !== "string" || json.accessToken.length === 0) {
61
+ throw new ManagedMaterialFetchError(
62
+ response.status,
63
+ "missing accessToken in CP response",
64
+ "Control-plane material response missing accessToken",
65
+ );
66
+ }
67
+ return {
68
+ accessToken: json.accessToken,
69
+ // CP intentionally never returns the refresh token to the workspace.
70
+ refreshToken: undefined,
71
+ expiresAt: typeof json.expiresAt === "string" ? json.expiresAt : undefined,
72
+ grantedScopes: Array.isArray(json.scopes) ? json.scopes.filter((s): s is string => typeof s === "string") : [],
73
+ };
74
+ }
75
+
76
+ async setMaterial(_ref: CredentialMaterialRef, _material: MaterialBundle): Promise<void> {
77
+ throw new ManagedCredentialMaterialWriteError();
78
+ }
79
+ }
@@ -4,10 +4,7 @@ import type { Clock, OAuthFlowExecutor, OAuthMaterial } from "@codemation/core";
4
4
  import { ApplicationTokens } from "../applicationTokens";
5
5
  import type { LoggerFactory } from "../application/logging/Logger";
6
6
  import { CredentialSecretCipher } from "../domain/credentials/CredentialSecretCipher";
7
- import type {
8
- CredentialOAuth2MaterialRecord,
9
- CredentialStore,
10
- } from "../domain/credentials/CredentialServices";
7
+ import type { CredentialOAuth2MaterialRecord, CredentialStore } from "../domain/credentials/CredentialServices";
11
8
 
12
9
  /**
13
10
  * Reads OAuth2 material for a credential instance and proactively refreshes it
@@ -128,9 +125,7 @@ export class CredentialOAuth2MaterialReader {
128
125
  refreshToken: typeof json.refreshToken === "string" ? json.refreshToken : undefined,
129
126
  expiresAt: typeof json.expiresAt === "string" ? json.expiresAt : undefined,
130
127
  grantedScopes:
131
- typeof json.grantedScopes === "string"
132
- ? json.grantedScopes.split(/\s+/).filter((s) => s.length > 0)
133
- : [],
128
+ typeof json.grantedScopes === "string" ? json.grantedScopes.split(/\s+/).filter((s) => s.length > 0) : [],
134
129
  };
135
130
  }
136
131
  }
@@ -0,0 +1,83 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import type { Hono } from "hono";
3
+ import type { Logger, LoggerFactory } from "../application/logging/Logger";
4
+ import { ApplicationRequestError } from "../application/ApplicationRequestError";
5
+ import { ApplicationTokens } from "../applicationTokens";
6
+ import { CredentialBindingService } from "../domain/credentials/CredentialServices";
7
+ import { InternalHmacAuthMiddleware } from "../pairing/InternalHmacAuthMiddleware";
8
+ import type { InternalHonoApiRouteRegistrar } from "../presentation/http/hono/InternalHonoApiRouteRegistrar";
9
+
10
+ /**
11
+ * Body shape pushed from the control-plane concierge when binding a
12
+ * credential instance to a workflow node slot.
13
+ *
14
+ * Note: the original story brief described the body as
15
+ * `{ credentialInstanceId, nodeId, slotName }`, but `CredentialBinding`
16
+ * requires a `workflowId` and the codebase uses `slotKey` (not `slotName`).
17
+ * The request shape was corrected here and on the matching concierge tool.
18
+ */
19
+ type CredentialBindingBody = Readonly<{
20
+ workflowId: string;
21
+ nodeId: string;
22
+ slotKey: string;
23
+ credentialInstanceId: string;
24
+ }>;
25
+
26
+ /**
27
+ * Registers POST /internal/credentials/binding — HMAC-verified endpoint that
28
+ * lets the control-plane concierge bind a credential instance to a workflow
29
+ * node slot on behalf of a workspace user.
30
+ */
31
+ @injectable()
32
+ export class InternalCredentialsBindingRegistrar implements InternalHonoApiRouteRegistrar {
33
+ private readonly logger: Logger;
34
+
35
+ constructor(
36
+ @inject(InternalHmacAuthMiddleware) private readonly hmacMiddleware: InternalHmacAuthMiddleware,
37
+ @inject(CredentialBindingService) private readonly credentialBindingService: CredentialBindingService,
38
+ @inject(ApplicationTokens.LoggerFactory) loggerFactory: LoggerFactory,
39
+ ) {
40
+ this.logger = loggerFactory.create("InternalCredentialsBindingRegistrar");
41
+ }
42
+
43
+ register(app: Hono): void {
44
+ app.post("/internal/credentials/binding", this.hmacMiddleware.handle(), async (c) => {
45
+ try {
46
+ const rawBody = c.get("body" as never) as string | undefined;
47
+ const body = (rawBody ? JSON.parse(rawBody) : await c.req.json()) as Partial<CredentialBindingBody>;
48
+
49
+ if (!body.workflowId || typeof body.workflowId !== "string") {
50
+ return c.json({ error: "workflowId is required" }, 400);
51
+ }
52
+ if (!body.nodeId || typeof body.nodeId !== "string") {
53
+ return c.json({ error: "nodeId is required" }, 400);
54
+ }
55
+ if (!body.slotKey || typeof body.slotKey !== "string") {
56
+ return c.json({ error: "slotKey is required" }, 400);
57
+ }
58
+ if (!body.credentialInstanceId || typeof body.credentialInstanceId !== "string") {
59
+ return c.json({ error: "credentialInstanceId is required" }, 400);
60
+ }
61
+
62
+ const binding = await this.credentialBindingService.upsertBinding({
63
+ workflowId: body.workflowId,
64
+ nodeId: body.nodeId,
65
+ slotKey: body.slotKey,
66
+ instanceId: body.credentialInstanceId,
67
+ });
68
+
69
+ this.logger.info(
70
+ `Credential binding upserted for workflow=${body.workflowId} node=${body.nodeId} slot=${body.slotKey}`,
71
+ );
72
+
73
+ return c.json({ ok: true, binding });
74
+ } catch (error) {
75
+ if (error instanceof ApplicationRequestError) {
76
+ return c.json(error.payload, error.status as 400 | 404);
77
+ }
78
+ this.logger.error("Credential binding handler error", error instanceof Error ? error : undefined);
79
+ return c.json({ error: "Internal server error" }, 500);
80
+ }
81
+ });
82
+ }
83
+ }