@codemation/host 0.7.0 → 0.9.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 (164) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/LICENSE +37 -1
  3. package/dist/{ApiPaths-Dv1dcHu_.js → ApiPaths-DCvrlIjg.js} +12 -1
  4. package/dist/{ApiPaths-Dv1dcHu_.js.map → ApiPaths-DCvrlIjg.js.map} +1 -1
  5. package/dist/{AppConfigFactory-Cx4qQvRk.js → AppConfigFactory-D4LL1aOR.js} +77 -297
  6. package/dist/AppConfigFactory-D4LL1aOR.js.map +1 -0
  7. package/dist/{AppConfigFactory-DnLoQ9Li.d.ts → AppConfigFactory-DncmwCD1.d.ts} +2918 -199
  8. package/dist/{AppContainerFactory-DqKYCRNP.js → AppContainerFactory-jpYXGZGe.js} +1733 -475
  9. package/dist/AppContainerFactory-jpYXGZGe.js.map +1 -0
  10. package/dist/{CodemationAppContext-CKVv9W9q.d.ts → CodemationAppContext-K51b7oXe.d.ts} +9 -3
  11. package/dist/{CodemationAuthoring.types-DA3G3s6d.d.ts → CodemationAuthoring.types-BXlXIl4K.d.ts} +9 -4
  12. package/dist/{CodemationAuthoring.types-NGkBcmmT.js → CodemationAuthoring.types-BteaR3Dc.js} +3 -2
  13. package/dist/CodemationAuthoring.types-BteaR3Dc.js.map +1 -0
  14. package/dist/{CodemationConfigNormalizer-BAKjetJ6.d.ts → CodemationConfigNormalizer-B4rDYC9h.d.ts} +3 -3
  15. package/dist/{CodemationConsumerConfigLoader-GYpBBvqE.js → CodemationConsumerConfigLoader-By-6tuGc.js} +3 -1
  16. package/dist/CodemationConsumerConfigLoader-By-6tuGc.js.map +1 -0
  17. package/dist/{CodemationConsumerConfigLoader-nxOqvv46.d.ts → CodemationConsumerConfigLoader-Dt4jyLx6.d.ts} +3 -2
  18. package/dist/{CodemationPluginListMerger-DKLAHT2b.d.ts → CodemationPluginListMerger-DS6I3Xe0.d.ts} +64 -27
  19. package/dist/{persistenceServer-C-hH4z6l.js → CodemationPostgresPrismaClientFactory-C7156Fe-.js} +2 -2
  20. package/dist/CodemationPostgresPrismaClientFactory-C7156Fe-.js.map +1 -0
  21. package/dist/CodemationPostgresPrismaClientFactory-CTNTPnDr.d.ts +9 -0
  22. package/dist/{CredentialContractsRegistry-Bq2bq28t.d.ts → CredentialContractsRegistry-Dgu-rEXi.d.ts} +16 -3
  23. package/dist/{CredentialServices-Be2I60Th.d.ts → CredentialServices-B3wPyp2y.d.ts} +4 -4
  24. package/dist/{CredentialServices-Dk8yypeL.js → CredentialServices-Bios0dM8.js} +10 -4
  25. package/dist/CredentialServices-Bios0dM8.js.map +1 -0
  26. package/dist/{InternalPingRegistrar-DY3kSfxP.js → InternalPingRegistrar-BavAAnvk.js} +19 -16
  27. package/dist/InternalPingRegistrar-BavAAnvk.js.map +1 -0
  28. package/dist/{ItemsInputNormalizer-_RwIfRIQ.d.ts → ItemsInputNormalizer-CFkfNMLt.d.ts} +1434 -1225
  29. package/dist/PrismaMigrationDeployer-DdEcXXVi.d.ts +14 -0
  30. package/dist/{PublicFrontendBootstrapFactory-CY2FS-5g.d.ts → PublicFrontendBootstrapFactory-ClEjZP74.d.ts} +2 -2
  31. package/dist/{PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts → PublicFrontendBootstrapJsonCodec-HNItQ7ol.d.ts} +6 -1
  32. package/dist/{TelemetryContracts-BtDx84Cp.d.ts → TelemetryContracts-DpZEODQM.d.ts} +2 -2
  33. package/dist/{WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts → WorkflowPolicyUiPresentationFactory-BNn2fvR_.d.ts} +2 -2
  34. package/dist/{WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js → WorkflowPolicyUiPresentationFactory-DfvD2VHk.js} +1 -1
  35. package/dist/{WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map → WorkflowPolicyUiPresentationFactory-DfvD2VHk.js.map} +1 -1
  36. package/dist/authoring.d.ts +4 -4
  37. package/dist/authoring.js +1 -1
  38. package/dist/client.d.ts +1 -1
  39. package/dist/client.js +1 -1
  40. package/dist/consumer.d.ts +5 -5
  41. package/dist/consumer.js +1 -1
  42. package/dist/credentials.d.ts +5 -5
  43. package/dist/credentials.js +1 -1
  44. package/dist/devServerSidecar.d.ts +2 -2
  45. package/dist/dto.d.ts +5 -5
  46. package/dist/{index-DilAYwnH.d.ts → index-ChIfeWzk.d.ts} +71 -28
  47. package/dist/index.d.ts +49 -17
  48. package/dist/index.js +106 -13
  49. package/dist/index.js.map +1 -0
  50. package/dist/infrastructure/persistence/PrismaMigrationOperations.d.ts +44 -0
  51. package/dist/infrastructure/persistence/PrismaMigrationOperations.js +302 -0
  52. package/dist/infrastructure/persistence/PrismaMigrationOperations.js.map +1 -0
  53. package/dist/mapping.d.ts +2 -2
  54. package/dist/mapping.js +1 -1
  55. package/dist/nextServer.d.ts +15 -39
  56. package/dist/nextServer.js +6 -6
  57. package/dist/pairing.d.ts +27 -8
  58. package/dist/pairing.js +19 -3
  59. package/dist/pairing.js.map +1 -0
  60. package/dist/{pairing.types-snfZ_OzB.d.ts → pairing.types-D9Bjn98U.d.ts} +1 -1
  61. package/dist/persistenceServer.d.ts +31 -7
  62. package/dist/persistenceServer.js +2 -2
  63. package/dist/{server-C4bS62rg.d.ts → server-B5trn7y4.d.ts} +5 -5
  64. package/dist/{server-Y7kxwtCK.js → server-BlG9qV5S.js} +5 -5
  65. package/dist/{server-Y7kxwtCK.js.map → server-BlG9qV5S.js.map} +1 -1
  66. package/dist/server.d.ts +10 -10
  67. package/dist/server.js +9 -9
  68. package/package.json +28 -25
  69. package/playwright.config.ts +8 -2
  70. package/playwright.scaffolded-dev.config.ts +8 -2
  71. package/prisma/migrations/20260526120000_credential_material_pointer/migration.sql +18 -0
  72. package/prisma/migrations/20260527120000_add_human_task/migration.sql +32 -0
  73. package/prisma/migrations/20260527130000_add_hitl_state_json/migration.sql +6 -0
  74. package/prisma/migrations/20260527130000_add_hmac_nonce/migration.sql +12 -0
  75. package/prisma/migrations.sqlite/20260526120000_credential_material_pointer/migration.sql +13 -0
  76. package/prisma/migrations.sqlite/20260527120000_add_human_task/migration.sql +30 -0
  77. package/prisma/migrations.sqlite/20260527130000_add_hitl_state_json/migration.sql +6 -0
  78. package/prisma/migrations.sqlite/20260527130000_add_hmac_nonce/migration.sql +9 -0
  79. package/prisma/schema.postgresql.prisma +48 -0
  80. package/prisma/schema.sqlite.prisma +48 -0
  81. package/prisma-generated/prisma-postgresql-client/edge.js +40 -6
  82. package/prisma-generated/prisma-postgresql-client/index-browser.js +36 -2
  83. package/prisma-generated/prisma-postgresql-client/index.d.ts +3179 -163
  84. package/prisma-generated/prisma-postgresql-client/index.js +40 -6
  85. package/prisma-generated/prisma-postgresql-client/package.json +1 -1
  86. package/prisma-generated/prisma-postgresql-client/schema.prisma +48 -0
  87. package/prisma-generated/prisma-sqlite-client/edge.js +40 -6
  88. package/prisma-generated/prisma-sqlite-client/index-browser.js +36 -2
  89. package/prisma-generated/prisma-sqlite-client/index.d.ts +3175 -163
  90. package/prisma-generated/prisma-sqlite-client/index.js +40 -6
  91. package/prisma-generated/prisma-sqlite-client/package.json +1 -1
  92. package/prisma-generated/prisma-sqlite-client/schema.prisma +48 -0
  93. package/src/application/contracts/CredentialContractsRegistry.ts +15 -0
  94. package/src/application/credentials/AppGalleryProjector.ts +69 -0
  95. package/src/application/hitl/DecideHumanTaskCommandHandler.ts +149 -0
  96. package/src/application/hitl/DecisionSchemaValidator.ts +22 -0
  97. package/src/application/hitl/HitlCallbackHandler.ts +96 -0
  98. package/src/application/mapping/WorkflowDefinitionMapper.ts +1 -3
  99. package/src/application/queries/CredentialQueryHandlers.ts +2 -0
  100. package/src/application/queries/GetCredentialAppsQuery.ts +4 -0
  101. package/src/application/queries/GetCredentialAppsQueryHandler.ts +27 -0
  102. package/src/application/telemetry/ResumeTelemetryContextForRun.ts +53 -0
  103. package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +9 -8
  104. package/src/applicationTokens.ts +11 -1
  105. package/src/auth/managed/ManagedCorsMiddleware.ts +20 -5
  106. package/src/bootstrap/AppContainerFactory.ts +121 -3
  107. package/src/bootstrap/runtime/HeadlessApiRuntime.ts +47 -0
  108. package/src/credentials/CachingCredentialMaterialProvider.ts +96 -0
  109. package/src/credentials/CompositeCredentialMaterialProvider.ts +47 -0
  110. package/src/credentials/ControlPlaneCatalogFetcher.ts +8 -28
  111. package/src/credentials/ControlPlaneCredentialMaterialProvider.ts +79 -0
  112. package/src/credentials/CredentialOAuth2MaterialReader.ts +2 -7
  113. package/src/credentials/InternalCredentialsBindingRegistrar.ts +83 -0
  114. package/src/credentials/LocalCredentialMaterialProvider.ts +92 -0
  115. package/src/domain/credentials/CredentialInstanceService.ts +5 -1
  116. package/src/domain/credentials/CredentialTypeRegistryImpl.ts +18 -4
  117. package/src/domain/workflows/WorkflowActivationPreflightRules.ts +7 -4
  118. package/src/dto.ts +2 -0
  119. package/src/hitl/ControlPlaneInboxChannel.ts +102 -0
  120. package/src/hitl/HitlResumeTokenSigner.ts +80 -0
  121. package/src/hitl/HitlTimeoutJobScheduler.ts +77 -0
  122. package/src/hitl/HitlTimeoutWorker.ts +138 -0
  123. package/src/hitl/InboxChannelResolver.ts +49 -0
  124. package/src/hitl/LocalInboxChannel.ts +37 -0
  125. package/src/index.ts +3 -0
  126. package/src/infrastructure/persistence/PrismaCredentialStore.ts +10 -0
  127. package/src/infrastructure/persistence/PrismaHmacNonceStore.ts +29 -0
  128. package/src/infrastructure/persistence/PrismaHumanTaskStore.ts +156 -0
  129. package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +53 -383
  130. package/src/infrastructure/persistence/PrismaMigrationOperations.ts +401 -0
  131. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +39 -0
  132. package/src/mcp/AgentMcpIntegrationImpl.ts +5 -1
  133. package/src/pairing/HmacNonceStore.ts +14 -0
  134. package/src/pairing/HmacNonceStoreToken.ts +4 -0
  135. package/src/pairing/HmacRequestSigner.ts +10 -1
  136. package/src/pairing/InMemoryHmacNonceStore.ts +24 -0
  137. package/src/pairing/IncomingHmacVerifier.ts +28 -12
  138. package/src/pairing/InternalHmacAuthMiddleware.ts +1 -1
  139. package/src/pairing/index.ts +3 -0
  140. package/src/presentation/config/CodemationAuthoring.types.ts +7 -1
  141. package/src/presentation/config/CodemationConfig.ts +6 -0
  142. package/src/presentation/http/ApiPaths.ts +14 -0
  143. package/src/presentation/http/HeadlessHttpServerFactory.ts +56 -0
  144. package/src/presentation/http/hono/HonoHttpAnonymousRoutePolicyRegistry.ts +4 -0
  145. package/src/presentation/http/hono/registrars/CredentialHonoApiRouteRegistrar.ts +1 -0
  146. package/src/presentation/http/hono/registrars/HitlDecideHonoApiRouteRegistrar.ts +54 -0
  147. package/src/presentation/http/hono/registrars/HitlInternalCallbackHonoApiRouteRegistrar.ts +33 -0
  148. package/src/presentation/http/hono/registrars/HitlResumeHonoApiRouteRegistrar.ts +43 -0
  149. package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +9 -0
  150. package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +1 -1
  151. package/src/presentation/server/CodemationConsumerConfigLoader.ts +7 -2
  152. package/src/presentation/websocket/WorkflowWebsocketServerFactory.ts +16 -0
  153. package/src/server.ts +7 -2
  154. package/src/workflows/InternalWorkflowTestRunRegistrar.ts +9 -0
  155. package/tsconfig.json +1 -0
  156. package/dist/AppConfigFactory-Cx4qQvRk.js.map +0 -1
  157. package/dist/AppContainerFactory-DqKYCRNP.js.map +0 -1
  158. package/dist/CodemationAuthoring.types-NGkBcmmT.js.map +0 -1
  159. package/dist/CodemationConsumerConfigLoader-GYpBBvqE.js.map +0 -1
  160. package/dist/CredentialServices-Dk8yypeL.js.map +0 -1
  161. package/dist/InternalPingRegistrar-DY3kSfxP.js.map +0 -1
  162. package/dist/persistenceServer-C-hH4z6l.js.map +0 -1
  163. package/dist/persistenceServer-CeTHtC6E.d.ts +0 -30
  164. 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(
@@ -393,7 +422,9 @@ export class AppContainerFactory {
393
422
  // Register the no-op publisher as a fallback so OtelExecutionTelemetryFactory can always
394
423
  // resolve the token. registerOperationalInfrastructure overrides this with the WS relay.
395
424
  container.registerInstance(ApplicationTokens.TelemetrySpanPublisher, NoOpTelemetrySpanPublisher);
396
- BootTimer.measure("appContainer.registerCoreInfrastructure", () => this.registerCoreInfrastructure(container, inputs));
425
+ BootTimer.measure("appContainer.registerCoreInfrastructure", () =>
426
+ this.registerCoreInfrastructure(container, inputs),
427
+ );
397
428
  BootTimer.measure("appContainer.registerRepositoriesAndBuses", () => this.registerRepositoriesAndBuses(container));
398
429
  BootTimer.measure("appContainer.registerApplicationServicesAndRoutes", () =>
399
430
  this.registerApplicationServicesAndRoutes(container, inputs.appConfig),
@@ -481,7 +512,7 @@ export class AppContainerFactory {
481
512
  });
482
513
  }
483
514
 
484
- private mergeConfigMcpServers(container: Container, appConfig: AppConfig): void {
515
+ private mergeConfigMcpServers(container: Container, appConfig: AppConfig): void {
485
516
  const catalog = container.resolve(McpServerCatalog);
486
517
  catalog.merge("config", appConfig.mcpServers ?? []);
487
518
  }
@@ -716,6 +747,7 @@ private mergeConfigMcpServers(container: Container, appConfig: AppConfig): void
716
747
  container.registerSingleton(CredentialRuntimeMaterialService, CredentialRuntimeMaterialService);
717
748
  container.registerSingleton(WorkflowCredentialNodeResolver, WorkflowCredentialNodeResolver);
718
749
  container.registerSingleton(CredentialInstanceService, CredentialInstanceService);
750
+ container.registerSingleton(AppGalleryProjector, AppGalleryProjector);
719
751
  container.registerSingleton(CredentialBindingService, CredentialBindingService);
720
752
  container.registerSingleton(WorkflowActivationPreflightRules, WorkflowActivationPreflightRules);
721
753
  container.registerSingleton(WorkflowActivationPreflight, WorkflowActivationPreflight);
@@ -738,6 +770,26 @@ private mergeConfigMcpServers(container: Container, appConfig: AppConfig): void
738
770
  container.registerSingleton(LocalOAuthFlowExecutor, LocalOAuthFlowExecutor);
739
771
  }
740
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);
741
793
  container.registerSingleton(CodemationFrontendAuthSnapshotFactory, CodemationFrontendAuthSnapshotFactory);
742
794
  container.registerSingleton(FrontendAppConfigFactory, FrontendAppConfigFactory);
743
795
  container.registerSingleton(PublicFrontendBootstrapFactory, PublicFrontendBootstrapFactory);
@@ -850,6 +902,7 @@ private mergeConfigMcpServers(container: Container, appConfig: AppConfig): void
850
902
  ),
851
903
  });
852
904
  container.registerSingleton(OtelExecutionTelemetryFactory, OtelExecutionTelemetryFactory);
905
+ container.registerSingleton(ResumeTelemetryContextForRun, ResumeTelemetryContextForRun);
853
906
  container.registerSingleton(InMemoryRunTraceContextRepository, InMemoryRunTraceContextRepository);
854
907
  container.registerSingleton(InMemoryTelemetrySpanStore, InMemoryTelemetrySpanStore);
855
908
  container.registerSingleton(InMemoryTelemetryArtifactStore, InMemoryTelemetryArtifactStore);
@@ -872,6 +925,32 @@ private mergeConfigMcpServers(container: Container, appConfig: AppConfig): void
872
925
  container.registerSingleton(PrismaWorkflowActivationRepository, PrismaWorkflowActivationRepository);
873
926
  container.registerSingleton(InMemoryWorkflowActivationRepository, InMemoryWorkflowActivationRepository);
874
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
+ });
875
954
  container.register(ApplicationTokens.WorkflowDefinitionRepository, {
876
955
  useFactory: instanceCachingFactory(
877
956
  (dependencyContainer) =>
@@ -988,7 +1067,23 @@ private mergeConfigMcpServers(container: Container, appConfig: AppConfig): void
988
1067
  }
989
1068
 
990
1069
  private registerPairingInfrastructure(container: Container, appConfig: AppConfig): void {
991
- const pairingConfig = new PairingConfigFactory().create(appConfig.env);
1070
+ const isManagedMode = appConfig.auth?.kind === "managed";
1071
+ let pairingConfig;
1072
+ try {
1073
+ pairingConfig = new PairingConfigFactory().create(appConfig.env);
1074
+ } catch (err) {
1075
+ if (isManagedMode) {
1076
+ // In managed mode the secret is required — let the error surface.
1077
+ throw err;
1078
+ }
1079
+ // In non-managed mode an invalid-but-present WORKSPACE_PAIRING_SECRET is a misconfiguration
1080
+ // warning, not a fatal error. Log and continue without pairing.
1081
+ const logger = container.resolve(ServerLoggerFactory).create("codemation.pairing");
1082
+ logger.warn(
1083
+ `WORKSPACE_PAIRING_SECRET is set but invalid — pairing disabled. ${err instanceof Error ? err.message : String(err)}`,
1084
+ );
1085
+ return;
1086
+ }
992
1087
  if (!pairingConfig) {
993
1088
  // Pairing is optional in non-production environments (local dev without CP integration).
994
1089
  // Emit a startup warning so operators know the workspace-mcp HMAC channel is inactive.
@@ -1006,18 +1101,33 @@ private mergeConfigMcpServers(container: Container, appConfig: AppConfig): void
1006
1101
  return;
1007
1102
  }
1008
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);
1009
1106
  container.registerSingleton(HmacRequestSigner, HmacRequestSigner);
1010
1107
  container.registerSingleton(PairedFetch, PairedFetch);
1108
+ // T6: Durable nonce store — PrismaHmacNonceStore survives process restarts.
1109
+ container.registerSingleton(HmacNonceStoreToken, PrismaHmacNonceStore);
1011
1110
  container.registerSingleton(IncomingHmacVerifier, IncomingHmacVerifier);
1012
1111
  container.registerSingleton(InternalHmacAuthMiddleware, InternalHmacAuthMiddleware);
1013
1112
  container.registerSingleton(BrokerClient, BrokerClient);
1014
1113
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalPingRegistrar);
1015
1114
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalCredentialsPushRegistrar);
1115
+ container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalCredentialsBindingRegistrar);
1016
1116
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalCredentialsListRegistrar);
1017
1117
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalWorkflowsListRegistrar);
1018
1118
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalWorkflowDetailRegistrar);
1019
1119
  container.registerSingleton(ApplicationTokens.InternalHonoApiRouteRegistrar, InternalWorkflowActivationRegistrar);
1020
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
+ );
1021
1131
  }
1022
1132
 
1023
1133
  private registerOperationalInfrastructure(container: Container): void {
@@ -1104,6 +1214,9 @@ private mergeConfigMcpServers(container: Container, appConfig: AppConfig): void
1104
1214
  binaryStorage,
1105
1215
  new LazyExecutionTelemetryFactory(() => container.resolve(OtelExecutionTelemetryFactory)),
1106
1216
  new CatalogBackedCostTrackingTelemetryFactory(new StaticCostCatalog(FrameworkCostCatalogEntries)),
1217
+ undefined,
1218
+ undefined,
1219
+ container,
1107
1220
  ),
1108
1221
  );
1109
1222
  this.registerRuntimeNodeActivationScheduler(container);
@@ -1141,6 +1254,8 @@ private mergeConfigMcpServers(container: Container, appConfig: AppConfig): void
1141
1254
  container.resolve(PrismaWorkflowActivationRepository),
1142
1255
  );
1143
1256
  container.registerInstance(ApplicationTokens.CredentialStore, container.resolve(PrismaCredentialStore));
1257
+ // HITL: wire PrismaHumanTaskStore now that PrismaDatabaseClientToken is available
1258
+ container.registerInstance(HumanTaskStoreToken, container.resolve(PrismaHumanTaskStore));
1144
1259
  container.registerInstance(ApplicationTokens.RunTraceContextRepository, runTraceContextRepository);
1145
1260
  container.registerInstance(ApplicationTokens.TelemetrySpanStore, telemetrySpanStore);
1146
1261
  container.registerInstance(ApplicationTokens.TelemetryArtifactStore, telemetryArtifactStore);
@@ -1153,6 +1268,9 @@ private mergeConfigMcpServers(container: Container, appConfig: AppConfig): void
1153
1268
  binaryStorage,
1154
1269
  new LazyExecutionTelemetryFactory(() => container.resolve(OtelExecutionTelemetryFactory)),
1155
1270
  new CatalogBackedCostTrackingTelemetryFactory(new StaticCostCatalog(FrameworkCostCatalogEntries)),
1271
+ undefined,
1272
+ undefined,
1273
+ container,
1156
1274
  ),
1157
1275
  );
1158
1276
  if (appConfig.scheduler.kind === "bullmq") {
@@ -0,0 +1,47 @@
1
+ import type { AppConfig } from "../../presentation/config/AppConfig";
2
+ import type { AppContainerFactory } from "../AppContainerFactory";
3
+ import { FrontendRuntime } from "./FrontendRuntime";
4
+ import { CodemationHonoApiApp } from "../../presentation/http/hono/CodemationHonoApiAppFactory";
5
+ import type { WorkflowWebsocketServerFactory } from "../../presentation/websocket/WorkflowWebsocketServerFactory";
6
+ import type { HeadlessHttpServerFactory } from "../../presentation/http/HeadlessHttpServerFactory";
7
+ import type { Logger } from "../../application/logging/Logger";
8
+
9
+ /**
10
+ * Boots the Codemation API + WebSocket servers without the Next.js UI process.
11
+ * Used by `codemation serve web --headless` for workspace pod containers where the
12
+ * UI is served externally (e.g. from the control-plane's customer-ui).
13
+ */
14
+ export class HeadlessApiRuntime {
15
+ constructor(
16
+ private readonly appContainerFactory: AppContainerFactory,
17
+ private readonly websocketServerFactory: WorkflowWebsocketServerFactory,
18
+ private readonly httpServerFactory: HeadlessHttpServerFactory,
19
+ private readonly logger: Logger,
20
+ ) {}
21
+
22
+ async start(appConfig: AppConfig): Promise<void> {
23
+ const port = Number(appConfig.env.PORT ?? 4001);
24
+
25
+ this.logger.info(`Starting codemation headless API runtime`);
26
+ this.logger.info(`HTTP port: ${port}, WS port: ${appConfig.webSocketPort}`);
27
+
28
+ const websocketServer = this.websocketServerFactory.create(appConfig);
29
+
30
+ const container = await this.appContainerFactory.create({
31
+ appConfig,
32
+ sharedWorkflowWebsocketServer: websocketServer,
33
+ });
34
+
35
+ await container.resolve(FrontendRuntime).start();
36
+
37
+ const honoApp = container.resolve(CodemationHonoApiApp);
38
+ const httpServer = this.httpServerFactory.create(honoApp, port, this.logger);
39
+
40
+ await new Promise<void>((resolve) => {
41
+ httpServer.listen(port, () => {
42
+ this.logger.info(`codemation headless API listening on port ${port}`);
43
+ resolve();
44
+ });
45
+ });
46
+ }
47
+ }
@@ -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,21 +26,20 @@ 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 /api/catalog/oauth-apps
36
- * GET /api/catalog/mcp-servers
37
- * GET /api/catalog/credential-types
34
+ * GET /internal/catalog/mcp-servers
35
+ * GET /internal/catalog/credential-types
38
36
  *
39
37
  * Failure semantics: a failure on one endpoint does NOT prevent updating the
40
38
  * others. Each endpoint's consecutive-failure counter and staleness escalation
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}/api/catalog/oauth-apps`),
168
- this.pairedFetch.get(`${base}/api/catalog/mcp-servers`),
169
- this.pairedFetch.get(`${base}/api/catalog/credential-types`),
157
+ const [mcpResult, credTypesResult] = await Promise.allSettled([
158
+ this.pairedFetch.get(`${base}/internal/catalog/mcp-servers`),
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
  }