@codemation/host 0.6.0 → 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 (223) hide show
  1. package/CHANGELOG.md +431 -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-C6q-CSKb.js → AppConfigFactory-Cx4qQvRk.js} +112 -52
  6. package/dist/AppConfigFactory-Cx4qQvRk.js.map +1 -0
  7. package/dist/{AppConfigFactory-YnveXE9k.d.ts → AppConfigFactory-DnLoQ9Li.d.ts} +8490 -5548
  8. package/dist/{AppContainerFactory-qaqc-R1D.js → AppContainerFactory-DqKYCRNP.js} +7641 -2083
  9. package/dist/AppContainerFactory-DqKYCRNP.js.map +1 -0
  10. package/dist/{CodemationAppContext-DRu1Dpri.d.ts → CodemationAppContext-CKVv9W9q.d.ts} +8 -4
  11. package/dist/{CodemationAuthoring.types-fBRppnmi.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-DVko3cVN.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-DJWr86f-.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-DGc-jfa2.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-DrMIDSw8.d.ts → CredentialContractsRegistry-Bq2bq28t.d.ts} +2 -2
  26. package/dist/{CredentialServices-UfvHB-rN.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-C-KHg9Mo.d.ts → ItemsInputNormalizer-_RwIfRIQ.d.ts} +89 -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-DbaNomrH.d.ts → TelemetryContracts-BtDx84Cp.d.ts} +13 -4
  42. package/dist/{WorkflowPolicyUiPresentationFactory-DQEY-h_S.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-CzK2KFuz.d.ts → WorkflowViewContracts-B7aFQcIw.d.ts} +10 -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-BbBk26m0.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-CmsIKnO9.js → persistenceServer-C-hH4z6l.js} +2 -2
  69. package/dist/{persistenceServer-CmsIKnO9.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-MUNGsBYK.d.ts → server-C4bS62rg.d.ts} +21 -6
  74. package/dist/{server-CJFfY67o.js → server-Y7kxwtCK.js} +7 -6
  75. package/dist/{server-CJFfY67o.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/20260519000000_workflow_audit_log/migration.sql +23 -0
  80. package/prisma/migrations/20260519100000_storage_growth_fixes/migration.sql +61 -0
  81. package/prisma/migrations.sqlite/20260519000000_workflow_audit_log/migration.sql +21 -0
  82. package/prisma/migrations.sqlite/20260519100000_storage_growth_fixes/migration.sql +29 -0
  83. package/prisma/schema.postgresql.prisma +55 -17
  84. package/prisma/schema.sqlite.prisma +55 -17
  85. package/prisma-generated/prisma-postgresql-client/edge.js +33 -5
  86. package/prisma-generated/prisma-postgresql-client/index-browser.js +29 -1
  87. package/prisma-generated/prisma-postgresql-client/index.d.ts +8933 -5716
  88. package/prisma-generated/prisma-postgresql-client/index.js +33 -5
  89. package/prisma-generated/prisma-postgresql-client/package.json +1 -1
  90. package/prisma-generated/prisma-postgresql-client/schema.prisma +38 -0
  91. package/prisma-generated/prisma-sqlite-client/edge.js +33 -5
  92. package/prisma-generated/prisma-sqlite-client/index-browser.js +29 -1
  93. package/prisma-generated/prisma-sqlite-client/index.d.ts +8925 -5713
  94. package/prisma-generated/prisma-sqlite-client/index.js +33 -5
  95. package/prisma-generated/prisma-sqlite-client/package.json +1 -1
  96. package/prisma-generated/prisma-sqlite-client/schema.prisma +38 -0
  97. package/scripts/check-collections.mjs +18 -0
  98. package/scripts/generate-prisma-clients.mjs +20 -11
  99. package/src/application/WorkflowAuditLogPruneScheduler.ts +96 -0
  100. package/src/application/auth/AuthenticatedPrincipal.ts +4 -0
  101. package/src/application/commands/StartWorkflowRunCommandHandler.ts +4 -0
  102. package/src/application/contracts/WorkflowViewContracts.ts +6 -0
  103. package/src/application/contracts/WorkflowWebsocketMessage.ts +3 -1
  104. package/src/application/mapping/WorkflowDefinitionMapper.ts +40 -1
  105. package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +7 -1
  106. package/src/application/telemetry/OtelExecutionTelemetry.types.ts +5 -0
  107. package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +4 -0
  108. package/src/application/telemetry/StoredTelemetrySpanScope.ts +6 -2
  109. package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +27 -17
  110. package/src/application/telemetry/TelemetrySpanPublisher.ts +11 -0
  111. package/src/application/websocket/TelemetrySpanWebsocketRelay.ts +31 -0
  112. package/src/applicationTokens.ts +20 -1
  113. package/src/audit/IAuditEmitter.ts +32 -0
  114. package/src/audit/PrismaWorkflowAuditLogRepository.ts +34 -0
  115. package/src/audit/WorkflowAuditLogWriter.ts +125 -0
  116. package/src/auth/managed/ManagedAuthConfig.ts +29 -0
  117. package/src/auth/managed/ManagedAuthMiddleware.ts +52 -0
  118. package/src/auth/managed/ManagedCorsMiddleware.ts +43 -0
  119. package/src/auth/managed/ManagedModeBootGuard.ts +27 -0
  120. package/src/auth/managed/index.ts +5 -0
  121. package/src/bootstrap/AppContainerFactory.ts +277 -29
  122. package/src/bootstrap/AppContainerLifecycle.ts +31 -0
  123. package/src/bootstrap/perf/BootTimer.ts +168 -0
  124. package/src/bootstrap/runtime/AppConfigFactory.ts +21 -65
  125. package/src/bootstrap/runtime/FrontendRuntime.ts +4 -1
  126. package/src/bootstrap/runtime/WorkerRuntime.ts +2 -1
  127. package/src/credentials/BrokerClient.ts +49 -0
  128. package/src/credentials/BrokerRefreshError.ts +12 -0
  129. package/src/credentials/BrokerRefreshInvalidGrantError.ts +13 -0
  130. package/src/credentials/ControlPlaneCatalogFetcher.ts +261 -0
  131. package/src/credentials/CredentialOAuth2MaterialReader.ts +136 -0
  132. package/src/credentials/InternalCredentialsListRegistrar.ts +48 -0
  133. package/src/credentials/InternalCredentialsPushRegistrar.ts +125 -0
  134. package/src/credentials/LocalOAuthFlowExecutor.ts +316 -0
  135. package/src/credentials/ManagedOAuthFlowExecutor.ts +94 -0
  136. package/src/credentials/ManagedOAuthRefreshInvalidGrantError.ts +13 -0
  137. package/src/credentials/catalogTypes.ts +4 -0
  138. package/src/credentials/refresh/CredentialDisconnectedError.ts +11 -0
  139. package/src/domain/credentials/CredentialBindingService.ts +54 -2
  140. package/src/domain/credentials/CredentialKeyRotatedError.ts +22 -0
  141. package/src/domain/credentials/CredentialSecretCipher.ts +68 -6
  142. package/src/domain/credentials/CredentialTypeRegistryImpl.ts +117 -10
  143. package/src/domain/credentials/OAuth2RedirectUriResolver.ts +79 -0
  144. package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +14 -5
  145. package/src/domain/telemetry/TelemetryContracts.ts +7 -1
  146. package/src/domain/workflows/WorkflowActivationPreflight.ts +24 -1
  147. package/src/domain/workflows/WorkflowActivationPreflightRules.ts +40 -1
  148. package/src/index.ts +6 -0
  149. package/src/infrastructure/binary/LocalFilesystemBinaryStorageRegistry.ts +29 -1
  150. package/src/infrastructure/binary/S3BinaryStorage.ts +169 -0
  151. package/src/infrastructure/binary/S3BinaryStorageConfig.ts +17 -0
  152. package/src/infrastructure/config/CodemationPluginRegistrar.ts +3 -1
  153. package/src/infrastructure/persistence/CodemationDatabaseUrlParser.ts +41 -0
  154. package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +8 -3
  155. package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +21 -13
  156. package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +43 -8
  157. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +26 -3
  158. package/src/infrastructure/persistence/PrismaWorkflowSnapshotRepository.ts +48 -0
  159. package/src/mcp/AgentMcpIntegrationImpl.ts +344 -0
  160. package/src/mcp/McpClientFactory.ts +29 -0
  161. package/src/mcp/McpConnectionPool.ts +184 -0
  162. package/src/mcp/McpConnectionPool.types.ts +12 -0
  163. package/src/mcp/McpServerCatalog.ts +104 -0
  164. package/src/mcp/index.ts +5 -0
  165. package/src/pairing/HmacRequestSigner.ts +32 -0
  166. package/src/pairing/IncomingHmacVerifier.ts +82 -0
  167. package/src/pairing/InternalHmacAuthMiddleware.ts +33 -0
  168. package/src/pairing/InternalPingRegistrar.ts +25 -0
  169. package/src/pairing/PairedFetch.ts +33 -0
  170. package/src/pairing/PairingConfigFactory.ts +35 -0
  171. package/src/pairing/PairingConfigToken.ts +6 -0
  172. package/src/pairing/index.ts +14 -0
  173. package/src/pairing/pairing.types.ts +18 -0
  174. package/src/pairing.ts +17 -0
  175. package/src/persistenceServer.ts +1 -0
  176. package/src/presentation/config/AppConfig.ts +7 -1
  177. package/src/presentation/config/CodemationAuthConfig.ts +1 -1
  178. package/src/presentation/config/CodemationAuthoring.types.ts +54 -5
  179. package/src/presentation/config/CodemationConfig.ts +3 -0
  180. package/src/presentation/config/CodemationConfigNormalizer.ts +39 -1
  181. package/src/presentation/config/CodemationPlugin.ts +2 -1
  182. package/src/presentation/frontend/CodemationFrontendAuthSnapshot.ts +5 -0
  183. package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -1
  184. package/src/presentation/frontend/PublicFrontendBootstrap.ts +2 -0
  185. package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +5 -1
  186. package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +4 -1
  187. package/src/presentation/http/ApiPaths.ts +4 -4
  188. package/src/presentation/http/ServerHttpErrorResponseFactory.ts +39 -2
  189. package/src/presentation/http/hono/CodemationHonoApiAppFactory.ts +33 -8
  190. package/src/presentation/http/hono/InternalHonoApiRouteRegistrar.ts +12 -0
  191. package/src/presentation/http/hono/registrars/ManagedMeHonoApiRouteRegistrar.ts +35 -0
  192. package/src/presentation/http/hono/registrars/OAuth2HonoApiRouteRegistrar.ts +2 -2
  193. package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +28 -0
  194. package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +98 -41
  195. package/src/presentation/server/CodemationConsumerConfigLoader.ts +54 -7
  196. package/src/presentation/server/CodemationPluginDiscovery.ts +5 -0
  197. package/src/presentation/server/WorkflowDefinitionExportsResolver.ts +18 -0
  198. package/src/presentation/server/WorkflowModulePathFinder.ts +12 -1
  199. package/src/presentation/websocket/ManagedWebsocketAuthenticator.ts +50 -0
  200. package/src/presentation/websocket/WebsocketAuthenticator.types.ts +12 -0
  201. package/src/presentation/websocket/WorkflowWebsocketServer.ts +24 -3
  202. package/src/process/ExecaProcessRunner.ts +41 -0
  203. package/src/process/ProcessRunner.types.ts +39 -0
  204. package/src/server.ts +2 -0
  205. package/src/workflows/InternalWorkflowActivationRegistrar.ts +42 -0
  206. package/src/workflows/InternalWorkflowDetailRegistrar.ts +33 -0
  207. package/src/workflows/InternalWorkflowTestRunRegistrar.ts +91 -0
  208. package/src/workflows/InternalWorkflowsListRegistrar.ts +28 -0
  209. package/src/workflows/discovery/WorkflowDirectoryDiscoverer.ts +79 -0
  210. package/tsconfig.json +2 -0
  211. package/vitest.shared.ts +5 -0
  212. package/dist/ApiPaths-CLTHphYZ.js.map +0 -1
  213. package/dist/AppConfigFactory-C6q-CSKb.js.map +0 -1
  214. package/dist/AppContainerFactory-qaqc-R1D.js.map +0 -1
  215. package/dist/CodemationAuthoring.types-DZl-sJaM.js.map +0 -1
  216. package/dist/CodemationConsumerConfigLoader-BeAUS144.js.map +0 -1
  217. package/dist/CredentialServices-CgxwguAv.js.map +0 -1
  218. package/dist/PublicFrontendBootstrapFactory-Cb2pLmDd.d.ts +0 -45
  219. package/dist/PublicFrontendBootstrapJsonCodec-DzqvA0uo.js.map +0 -1
  220. package/dist/ServerLoggerFactory-BKSIh9Xv.js +0 -98
  221. package/dist/ServerLoggerFactory-BKSIh9Xv.js.map +0 -1
  222. package/dist/persistenceServer-vtJAGDat.d.ts +0 -9
  223. package/src/domain/credentials/OAuth2ConnectServiceFactory.ts +0 -411
@@ -1,6 +1,9 @@
1
1
  import { ApplicationRequestError } from "../../application/ApplicationRequestError";
2
- import { CoreTokens, inject, injectable, type WorkflowRepository } from "@codemation/core";
2
+ import { ApplicationTokens } from "../../applicationTokens";
3
+ import { CoreTokens, inject, injectable, type CredentialTypeRegistry, type WorkflowRepository } from "@codemation/core";
3
4
  import { CredentialBindingService } from "../credentials/CredentialServices";
5
+ import { CredentialOAuth2ScopeResolver } from "../credentials/CredentialOAuth2ScopeResolver";
6
+ import type { CredentialStore } from "../credentials/CredentialServices";
4
7
  import { WorkflowActivationPreflightRules } from "./WorkflowActivationPreflightRules";
5
8
 
6
9
  @injectable()
@@ -12,6 +15,12 @@ export class WorkflowActivationPreflight {
12
15
  private readonly credentialBindingService: CredentialBindingService,
13
16
  @inject(WorkflowActivationPreflightRules)
14
17
  private readonly rules: WorkflowActivationPreflightRules,
18
+ @inject(CoreTokens.CredentialTypeRegistry)
19
+ private readonly credentialTypeRegistry: CredentialTypeRegistry,
20
+ @inject(ApplicationTokens.CredentialStore)
21
+ private readonly credentialStore: CredentialStore,
22
+ @inject(CredentialOAuth2ScopeResolver)
23
+ private readonly credentialOAuth2ScopeResolver: CredentialOAuth2ScopeResolver,
15
24
  ) {}
16
25
 
17
26
  async assertCanActivate(workflowId: string): Promise<void> {
@@ -21,9 +30,23 @@ export class WorkflowActivationPreflight {
21
30
  throw new ApplicationRequestError(404, `Unknown workflowId: ${decodedId}`);
22
31
  }
23
32
  const health = await this.credentialBindingService.listWorkflowHealth(decodedId);
33
+ const scopeErrors = await this.rules.collectScopeMismatchErrors(health, {
34
+ getRequiredScopes: (typeId, _requirement) => {
35
+ const type = this.credentialTypeRegistry.getType(typeId);
36
+ if (type?.auth?.kind === "oauth2") {
37
+ return this.credentialOAuth2ScopeResolver.resolveRequestedScopes(type.auth, {});
38
+ }
39
+ return [];
40
+ },
41
+ getGrantedScopes: async (instanceId) => {
42
+ const material = await this.credentialStore.getOAuth2Material(instanceId);
43
+ return material?.scopes ?? [];
44
+ },
45
+ });
24
46
  const errors = [
25
47
  ...this.rules.collectNonManualTriggerErrors(workflow),
26
48
  ...this.rules.collectRequiredCredentialErrors(health),
49
+ ...scopeErrors,
27
50
  ];
28
51
  if (errors.length > 0) {
29
52
  throw new ApplicationRequestError(400, "Workflow cannot be activated.", errors);
@@ -1,5 +1,5 @@
1
1
  import type { WorkflowCredentialHealthDto } from "../../application/contracts/CredentialContractsRegistry";
2
- import { getPersistedRuntimeTypeMetadata, injectable, type WorkflowDefinition } from "@codemation/core";
2
+ import { getPersistedRuntimeTypeMetadata, injectable, type CredentialRequirement, type WorkflowDefinition } from "@codemation/core";
3
3
  import { MissingRuntimeTriggerToken } from "@codemation/core/bootstrap";
4
4
  import { ManualTriggerNode } from "@codemation/core-nodes";
5
5
 
@@ -74,4 +74,43 @@ export class WorkflowActivationPreflightRules {
74
74
  }
75
75
  return lines;
76
76
  }
77
+
78
+ async collectScopeMismatchErrors(
79
+ health: WorkflowCredentialHealthDto,
80
+ opts: {
81
+ getRequiredScopes: (typeId: string, slotRequirement: CredentialRequirement) => ReadonlyArray<string>;
82
+ getGrantedScopes: (instanceId: string) => Promise<ReadonlyArray<string>>;
83
+ },
84
+ ): Promise<ReadonlyArray<string>> {
85
+ const checked = new Set<string>();
86
+ const lines: string[] = [];
87
+
88
+ for (const slot of health.slots) {
89
+ const { instance } = slot;
90
+ if (!instance) {
91
+ continue;
92
+ }
93
+ const { instanceId, typeId, displayName } = instance;
94
+ if (checked.has(instanceId)) {
95
+ continue;
96
+ }
97
+ checked.add(instanceId);
98
+
99
+ const required = opts.getRequiredScopes(typeId, slot.requirement);
100
+ if (required.length === 0) {
101
+ continue;
102
+ }
103
+
104
+ const granted = await opts.getGrantedScopes(instanceId);
105
+ const grantedSet = new Set(granted);
106
+ const missing = required.filter((s) => !grantedSet.has(s));
107
+ if (missing.length > 0) {
108
+ lines.push(
109
+ `Credential "${displayName}" missing scopes: ${missing.join(", ")}. Reconnect to grant.`,
110
+ );
111
+ }
112
+ }
113
+
114
+ return lines;
115
+ }
77
116
  }
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@ export { UpsertLocalBootstrapUserCommand } from "./application/commands/UpsertLo
6
6
  export type { UpsertLocalBootstrapUserResultDto } from "./application/contracts/userDirectoryContracts.types";
7
7
  export { AppContainerFactory } from "./bootstrap/AppContainerFactory";
8
8
  export { AppContainerLifecycle } from "./bootstrap/AppContainerLifecycle";
9
+ export { BootTimer } from "./bootstrap/perf/BootTimer";
10
+ export type { BootTracePhase } from "./bootstrap/perf/BootTimer";
9
11
  export { DatabaseMigrations } from "./bootstrap/runtime/DatabaseMigrations";
10
12
  export { CollectionSchemaSyncerHolder } from "./infrastructure/collections/CollectionSchemaSyncerHolder";
11
13
  export { FrontendRuntime } from "./bootstrap/runtime/FrontendRuntime";
@@ -56,6 +58,10 @@ export { InsertCollectionRowCommand } from "./application/collections/InsertColl
56
58
  export { UpdateCollectionRowCommand } from "./application/collections/UpdateCollectionRowCommand";
57
59
  export { DeleteCollectionRowCommand } from "./application/collections/DeleteCollectionRowCommand";
58
60
  export { SyncCollectionsCommand } from "./application/collections/SyncCollectionsCommand";
61
+ export { StartWorkflowRunCommand } from "./application/commands/StartWorkflowRunCommand";
62
+ export type { RunCommandResult } from "./application/contracts/RunContracts";
63
+ export { ApplicationRequestError } from "./application/ApplicationRequestError";
64
+ export { GetRunStateQuery } from "./application/queries/GetRunStateQuery";
59
65
 
60
66
  export type {
61
67
  CodemationFrontendAuthProviderSnapshot,
@@ -1,6 +1,6 @@
1
1
  import { createReadStream, createWriteStream } from "node:fs";
2
2
 
3
- import { mkdir, rm, stat } from "node:fs/promises";
3
+ import { mkdir, readdir, rm, stat } from "node:fs/promises";
4
4
 
5
5
  import path from "node:path";
6
6
 
@@ -72,6 +72,34 @@ export class LocalFilesystemBinaryStorage implements BinaryStorage {
72
72
  await rm(this.resolveAbsolutePath(storageKey), { force: true });
73
73
  }
74
74
 
75
+ async deleteMany(storageKeys: ReadonlyArray<string>): Promise<void> {
76
+ await Promise.all(storageKeys.map((key) => this.delete(key)));
77
+ }
78
+
79
+ async listByPrefix(prefix: string): Promise<ReadonlyArray<string>> {
80
+ const results: string[] = [];
81
+ await this.collectKeysWithPrefix(prefix, this.baseDirectory, results);
82
+ return results;
83
+ }
84
+
85
+ private async collectKeysWithPrefix(prefix: string, dir: string, results: string[]): Promise<void> {
86
+ let entries;
87
+ try {
88
+ entries = await readdir(dir, { withFileTypes: true });
89
+ } catch {
90
+ return;
91
+ }
92
+ for (const entry of entries) {
93
+ const entryPath = path.join(dir, entry.name);
94
+ const relKey = path.relative(path.resolve(this.baseDirectory), entryPath).replace(/\\/g, "/");
95
+ if (entry.isDirectory()) {
96
+ await this.collectKeysWithPrefix(prefix, entryPath, results);
97
+ } else if (relKey.startsWith(prefix)) {
98
+ results.push(relKey);
99
+ }
100
+ }
101
+ }
102
+
75
103
  private resolveAbsolutePath(storageKey: string): string {
76
104
  const absoluteBaseDirectory = path.resolve(this.baseDirectory);
77
105
  const targetPath = path.resolve(absoluteBaseDirectory, storageKey);
@@ -0,0 +1,169 @@
1
+ import { PassThrough, Readable } from "node:stream";
2
+
3
+ import {
4
+ DeleteObjectCommand,
5
+ DeleteObjectsCommand,
6
+ HeadBucketCommand,
7
+ HeadObjectCommand,
8
+ ListObjectsV2Command,
9
+ S3Client,
10
+ } from "@aws-sdk/client-s3";
11
+
12
+ import { Upload } from "@aws-sdk/lib-storage";
13
+
14
+ import type {
15
+ BinaryBody,
16
+ BinaryStorage,
17
+ BinaryStorageReadResult,
18
+ BinaryStorageStatResult,
19
+ BinaryStorageWriteResult,
20
+ } from "@codemation/core";
21
+
22
+ import { BinaryBodyNodeReadableFactory } from "./BinaryBodyNodeReadableFactory";
23
+ import type { S3BinaryStorageConfig } from "./S3BinaryStorageConfig";
24
+
25
+ const DELETE_BATCH_SIZE = 1000;
26
+
27
+ export class S3BinaryStorage implements BinaryStorage {
28
+ readonly driverName = "s3";
29
+
30
+ private readonly client: S3Client;
31
+ private readonly bucket: string;
32
+
33
+ /**
34
+ * @param config - S3 connection details.
35
+ * @param forcePathStyle - Use path-style addressing (true for MinIO / testcontainers; false for Scaleway). Default false.
36
+ */
37
+ constructor(config: S3BinaryStorageConfig, forcePathStyle = false) {
38
+ this.bucket = config.bucket;
39
+ this.client = new S3Client({
40
+ endpoint: config.endpoint,
41
+ region: config.region,
42
+ forcePathStyle,
43
+ credentials: {
44
+ accessKeyId: config.accessKeyId,
45
+ secretAccessKey: config.secretAccessKey,
46
+ },
47
+ });
48
+ }
49
+
50
+ async write(args: { storageKey: string; body: BinaryBody }): Promise<BinaryStorageWriteResult> {
51
+ const readable = new BinaryBodyNodeReadableFactory(args.body).create();
52
+ let size = 0;
53
+ const passThrough = new PassThrough();
54
+ readable.on("data", (chunk: Buffer) => {
55
+ size += chunk.byteLength;
56
+ });
57
+ readable.pipe(passThrough);
58
+
59
+ const upload = new Upload({
60
+ client: this.client,
61
+ params: {
62
+ Bucket: this.bucket,
63
+ Key: args.storageKey,
64
+ Body: passThrough,
65
+ },
66
+ });
67
+
68
+ await upload.done();
69
+
70
+ return {
71
+ storageKey: args.storageKey,
72
+ size,
73
+ };
74
+ }
75
+
76
+ async openReadStream(storageKey: string): Promise<BinaryStorageReadResult | undefined> {
77
+ const { GetObjectCommand } = await import("@aws-sdk/client-s3");
78
+ let response;
79
+ try {
80
+ response = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: storageKey }));
81
+ } catch (err) {
82
+ if (this.isNotFoundError(err)) {
83
+ return undefined;
84
+ }
85
+ throw err;
86
+ }
87
+
88
+ if (!response.Body) {
89
+ return undefined;
90
+ }
91
+
92
+ const nodeReadable = Readable.from(response.Body as AsyncIterable<Uint8Array>);
93
+ return {
94
+ body: Readable.toWeb(nodeReadable) as BinaryStorageReadResult["body"],
95
+ size: response.ContentLength,
96
+ };
97
+ }
98
+
99
+ async stat(storageKey: string): Promise<BinaryStorageStatResult> {
100
+ try {
101
+ const response = await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: storageKey }));
102
+ return { exists: true, size: response.ContentLength };
103
+ } catch (err) {
104
+ if (this.isNotFoundError(err)) {
105
+ return { exists: false };
106
+ }
107
+ throw err;
108
+ }
109
+ }
110
+
111
+ async delete(storageKey: string): Promise<void> {
112
+ await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: storageKey }));
113
+ }
114
+
115
+ async deleteMany(storageKeys: ReadonlyArray<string>): Promise<void> {
116
+ for (let i = 0; i < storageKeys.length; i += DELETE_BATCH_SIZE) {
117
+ const batch = storageKeys.slice(i, i + DELETE_BATCH_SIZE);
118
+ await this.client.send(
119
+ new DeleteObjectsCommand({
120
+ Bucket: this.bucket,
121
+ Delete: {
122
+ Objects: batch.map((key) => ({ Key: key })),
123
+ Quiet: true,
124
+ },
125
+ }),
126
+ );
127
+ }
128
+ }
129
+
130
+ async listByPrefix(prefix: string): Promise<ReadonlyArray<string>> {
131
+ const keys: string[] = [];
132
+ let continuationToken: string | undefined;
133
+
134
+ do {
135
+ const response = await this.client.send(
136
+ new ListObjectsV2Command({
137
+ Bucket: this.bucket,
138
+ Prefix: prefix,
139
+ ContinuationToken: continuationToken,
140
+ }),
141
+ );
142
+
143
+ for (const obj of response.Contents ?? []) {
144
+ if (obj.Key) {
145
+ keys.push(obj.Key);
146
+ }
147
+ }
148
+
149
+ continuationToken = response.NextContinuationToken;
150
+ } while (continuationToken);
151
+
152
+ return keys;
153
+ }
154
+
155
+ /** Checks that the configured bucket is reachable. Throws if not. */
156
+ async checkConnectivity(): Promise<void> {
157
+ await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }));
158
+ }
159
+
160
+ private isNotFoundError(err: unknown): boolean {
161
+ if (typeof err !== "object" || err === null) {
162
+ return false;
163
+ }
164
+ const anyErr = err as Record<string, unknown>;
165
+ const statusCode =
166
+ anyErr["$metadata"] != null ? (anyErr["$metadata"] as Record<string, unknown>)["httpStatusCode"] : undefined;
167
+ return statusCode === 404 || anyErr["name"] === "NotFound" || anyErr["name"] === "NoSuchKey";
168
+ }
169
+ }
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+
3
+ export interface S3BinaryStorageConfig {
4
+ readonly endpoint: string;
5
+ readonly region: string;
6
+ readonly bucket: string;
7
+ readonly accessKeyId: string;
8
+ readonly secretAccessKey: string;
9
+ }
10
+
11
+ export const S3BinaryStorageConfigSchema = z.object({
12
+ endpoint: z.string().min(1),
13
+ region: z.string().min(1),
14
+ bucket: z.string().min(1),
15
+ accessKeyId: z.string().min(1),
16
+ secretAccessKey: z.string().min(1),
17
+ });
@@ -1,4 +1,4 @@
1
- import type { AnyCredentialType, CollectionDefinition, Container } from "@codemation/core";
1
+ import type { AnyCredentialType, CollectionDefinition, Container, McpServerDeclaration } from "@codemation/core";
2
2
  import type { LoggerFactory } from "../../application/logging/Logger";
3
3
  import type { AppConfig } from "../../presentation/config/AppConfig";
4
4
  import type { CodemationPlugin } from "../../presentation/config/CodemationPlugin";
@@ -11,6 +11,7 @@ export class CodemationPluginRegistrar {
11
11
  appConfig: AppConfig;
12
12
  registerCredentialType: (type: AnyCredentialType) => void;
13
13
  registerCollection: (definition: CollectionDefinition) => void;
14
+ mergeMcpServers: (declarations: ReadonlyArray<McpServerDeclaration>) => void;
14
15
  loggerFactory: LoggerFactory;
15
16
  }>,
16
17
  ): Promise<void> {
@@ -18,6 +19,7 @@ export class CodemationPluginRegistrar {
18
19
  for (const credentialType of plugin.credentialTypes ?? []) {
19
20
  args.registerCredentialType(credentialType);
20
21
  }
22
+ args.mergeMcpServers(plugin.mcpServers ?? []);
21
23
  if (!plugin.register) {
22
24
  continue;
23
25
  }
@@ -0,0 +1,41 @@
1
+ import path from "node:path";
2
+ import type { AppPersistenceConfig } from "../../presentation/config/AppConfig";
3
+
4
+ /**
5
+ * Parses `CODEMATION_DATABASE_URL` into an {@link AppPersistenceConfig}.
6
+ *
7
+ * Supported schemes (case-insensitive):
8
+ * - `sqlite://relative/path/to/file.db` → resolved relative to consumerRoot
9
+ * - `sqlite:///absolute/path/to/file.db` → leading slash = POSIX absolute
10
+ * - `sqlite://C:/path/file.db` → Windows-style absolute (path.isAbsolute()
11
+ * returns true for these)
12
+ * - `pgsql://user:pass@host:5432/dbname` → normalised to postgresql://
13
+ * - `postgresql://user:pass@host:5432/db` → pass-through (Prisma's expected scheme)
14
+ * - `postgres://user:pass@host:5432/db` → pass-through (common alias)
15
+ *
16
+ * Throws on any other scheme. Empty / whitespace input is also an error — callers
17
+ * should default before calling parse().
18
+ */
19
+ export class CodemationDatabaseUrlParser {
20
+ parse(url: string, consumerRoot: string): AppPersistenceConfig {
21
+ const trimmed = url.trim();
22
+ if (trimmed.length === 0) {
23
+ throw new Error("CODEMATION_DATABASE_URL is empty.");
24
+ }
25
+ if (trimmed.toLowerCase().startsWith("sqlite://")) {
26
+ const remainder = trimmed.slice("sqlite://".length);
27
+ const filePath = path.isAbsolute(remainder) ? remainder : path.resolve(consumerRoot, remainder);
28
+ return { kind: "sqlite", databaseFilePath: filePath };
29
+ }
30
+ if (trimmed.toLowerCase().startsWith("pgsql://")) {
31
+ return { kind: "postgresql", databaseUrl: `postgresql://${trimmed.slice("pgsql://".length)}` };
32
+ }
33
+ if (trimmed.toLowerCase().startsWith("postgresql://") || trimmed.toLowerCase().startsWith("postgres://")) {
34
+ return { kind: "postgresql", databaseUrl: trimmed };
35
+ }
36
+ throw new Error(
37
+ `Unsupported CODEMATION_DATABASE_URL scheme: "${trimmed}". ` +
38
+ `Use sqlite://, pgsql://, postgresql://, or postgres://.`,
39
+ );
40
+ }
41
+ }
@@ -4,6 +4,7 @@ import type {
4
4
  TelemetryArtifactRecord,
5
5
  TelemetryArtifactStore,
6
6
  TelemetryArtifactWrite,
7
+ TelemetryPruneArgs,
7
8
  } from "../../domain/telemetry/TelemetryContracts";
8
9
 
9
10
  @injectable()
@@ -43,14 +44,18 @@ export class InMemoryTelemetryArtifactStore implements TelemetryArtifactStore {
43
44
  .sort((left, right) => left.createdAt.localeCompare(right.createdAt));
44
45
  }
45
46
 
46
- async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
47
+ async pruneExpired(args: TelemetryPruneArgs): Promise<{ count: number; storageKeys: ReadonlyArray<string> }> {
47
48
  const candidates = [...this.rows.entries()]
48
49
  .filter(([, row]) => row.retentionExpiresAt !== undefined && row.retentionExpiresAt <= args.nowIso)
49
50
  .sort((left, right) => (left[1].retentionExpiresAt ?? "").localeCompare(right[1].retentionExpiresAt ?? ""))
50
51
  .slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
51
- for (const [key] of candidates) {
52
+ const storageKeys: string[] = [];
53
+ for (const [key, row] of candidates) {
54
+ if (row.payloadStorageKey) {
55
+ storageKeys.push(row.payloadStorageKey);
56
+ }
52
57
  this.rows.delete(key);
53
58
  }
54
- return candidates.length;
59
+ return { count: candidates.length, storageKeys };
55
60
  }
56
61
  }
@@ -1,4 +1,4 @@
1
- import { createClient, type Client } from "@libsql/client";
1
+ import type { Client } from "@libsql/client";
2
2
  import { injectable } from "@codemation/core";
3
3
  import { spawn } from "node:child_process";
4
4
  import { existsSync } from "node:fs";
@@ -105,11 +105,12 @@ export class PrismaMigrationDeployer {
105
105
  env?: Readonly<NodeJS.ProcessEnv>;
106
106
  }>,
107
107
  ): Promise<void> {
108
- const prismaConfigPath = this.resolveAbsolutePrismaConfigPath();
108
+ const resolverEnv = { ...process.env, ...(args.env ?? {}) };
109
+ const prismaConfigPath = this.resolveAbsolutePrismaConfigPath(resolverEnv);
109
110
  await new Promise<void>((resolve, reject) => {
110
111
  const command = spawn(
111
112
  process.execPath,
112
- [...[this.resolvePrismaCliPath(), ...args.prismaArgs], "--config", path.basename(prismaConfigPath)],
113
+ [...[this.resolvePrismaCliPath(resolverEnv), ...args.prismaArgs], "--config", path.basename(prismaConfigPath)],
113
114
  {
114
115
  cwd: path.dirname(prismaConfigPath),
115
116
  env: this.createProcessEnvironment(args.databaseUrl, args.provider, args.env),
@@ -177,6 +178,10 @@ export class PrismaMigrationDeployer {
177
178
  }
178
179
 
179
180
  private async repairPartiallyAppliedNormalizedRuntimeSqliteDatabase(databaseFilePath: string): Promise<boolean> {
181
+ // Lazy import: @libsql/client pulls in platform-specific native bindings that confuse the
182
+ // Next.js / Turbopack module tracer (forcing the whole project to be traced via NFT). This
183
+ // recovery path is rarely needed, so defer the load until it's actually invoked.
184
+ const { createClient } = await import("@libsql/client");
180
185
  const client = createClient({ url: this.sqliteFilePathToDatabaseUrl(databaseFilePath) });
181
186
  try {
182
187
  const failedMigration = await this.hasActiveFailedMigrationRecord(
@@ -288,6 +293,7 @@ export class PrismaMigrationDeployer {
288
293
  }
289
294
 
290
295
  private async cleanupNormalizedRuntimeLegacyArtifacts(databaseFilePath: string): Promise<void> {
296
+ const { createClient } = await import("@libsql/client");
291
297
  const client = createClient({ url: this.sqliteFilePathToDatabaseUrl(databaseFilePath) });
292
298
  try {
293
299
  const runColumns = await this.readSqliteTableColumns(client, "Run");
@@ -326,14 +332,15 @@ export class PrismaMigrationDeployer {
326
332
  };
327
333
  }
328
334
 
329
- private resolvePrismaCliPath(): string {
330
- const configuredPath = process.env.CODEMATION_PRISMA_CLI_PATH;
335
+ private resolvePrismaCliPath(env: Readonly<NodeJS.ProcessEnv>): string {
336
+ const configuredPath = env.CODEMATION_PRISMA_CLI_PATH;
331
337
  if (configuredPath && existsSync(configuredPath)) {
332
338
  return configuredPath;
333
339
  }
340
+ const packageRoot = this.resolvePackageRoot(env);
334
341
  const packageManagerCandidates = [
335
342
  path.resolve(process.cwd(), "node_modules", "prisma", "build", "index.js"),
336
- path.resolve(this.resolvePackageRoot(), "node_modules", "prisma", "build", "index.js"),
343
+ path.resolve(packageRoot, "node_modules", "prisma", "build", "index.js"),
337
344
  ];
338
345
  for (const candidate of packageManagerCandidates) {
339
346
  if (existsSync(candidate)) {
@@ -342,7 +349,7 @@ export class PrismaMigrationDeployer {
342
349
  }
343
350
  try {
344
351
  return this.require.resolve("prisma/build/index.js", {
345
- paths: [process.cwd(), this.resolvePackageRoot()],
352
+ paths: [process.cwd(), packageRoot],
346
353
  });
347
354
  } catch {
348
355
  throw new Error(
@@ -351,16 +358,17 @@ export class PrismaMigrationDeployer {
351
358
  }
352
359
  }
353
360
 
354
- private resolveAbsolutePrismaConfigPath(): string {
355
- const configuredPath = process.env.CODEMATION_PRISMA_CONFIG_PATH;
361
+ private resolveAbsolutePrismaConfigPath(env: Readonly<NodeJS.ProcessEnv>): string {
362
+ const configuredPath = env.CODEMATION_PRISMA_CONFIG_PATH;
363
+ const packageRoot = this.resolvePackageRoot(env);
356
364
  if (configuredPath) {
357
- return path.isAbsolute(configuredPath) ? configuredPath : path.resolve(this.resolvePackageRoot(), configuredPath);
365
+ return path.isAbsolute(configuredPath) ? configuredPath : path.resolve(packageRoot, configuredPath);
358
366
  }
359
- return path.resolve(this.resolvePackageRoot(), "prisma.config.ts");
367
+ return path.resolve(packageRoot, "prisma.config.ts");
360
368
  }
361
369
 
362
- resolvePackageRoot(): string {
363
- const configuredRoot = process.env.CODEMATION_HOST_PACKAGE_ROOT;
370
+ resolvePackageRoot(env: Readonly<NodeJS.ProcessEnv> = process.env): string {
371
+ const configuredRoot = env.CODEMATION_HOST_PACKAGE_ROOT;
364
372
  if (configuredRoot) {
365
373
  return configuredRoot;
366
374
  }
@@ -1,12 +1,17 @@
1
- import { inject, injectable } from "@codemation/core";
1
+ import type { BinaryBody, BinaryStorage } from "@codemation/core";
2
+ import { CoreTokens, inject, injectable } from "@codemation/core";
2
3
  import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
3
4
  import type {
4
5
  TelemetryArtifactRecord,
5
6
  TelemetryArtifactStore,
6
7
  TelemetryArtifactWrite,
8
+ TelemetryPruneArgs,
7
9
  } from "../../domain/telemetry/TelemetryContracts";
8
10
  import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
9
11
 
12
+ /** Payloads larger than this byte threshold are offloaded to BinaryStorage. */
13
+ const PAYLOAD_OFFLOAD_THRESHOLD_BYTES = 64_000;
14
+
10
15
  @injectable()
11
16
  export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
12
17
  constructor(
@@ -14,11 +19,36 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
14
19
  private readonly prisma: PrismaDatabaseClient,
15
20
  @inject(OtelIdentityFactory)
16
21
  private readonly otelIdentityFactory: OtelIdentityFactory,
22
+ @inject(CoreTokens.BinaryStorage)
23
+ private readonly binaryStorage: BinaryStorage,
17
24
  ) {}
18
25
 
19
26
  async save(record: TelemetryArtifactWrite): Promise<TelemetryArtifactRecord> {
20
27
  const artifactId = this.otelIdentityFactory.createArtifactId();
21
28
  const createdAt = new Date().toISOString();
29
+
30
+ // Resolve inline vs offloaded payload
31
+ let payloadText: string | null = record.payloadText ?? null;
32
+ let payloadJson: string | null = record.payloadJson !== undefined ? JSON.stringify(record.payloadJson) : null;
33
+ let payloadStorageKey: string | null = null;
34
+
35
+ const payloadTextBytes = payloadText ? Buffer.byteLength(payloadText, "utf8") : 0;
36
+ const payloadJsonBytes = payloadJson ? Buffer.byteLength(payloadJson, "utf8") : 0;
37
+
38
+ if (payloadTextBytes > PAYLOAD_OFFLOAD_THRESHOLD_BYTES) {
39
+ const storageKey = `telemetry-artifacts/${artifactId}.txt`;
40
+ const body: BinaryBody = Buffer.from(payloadText!, "utf8");
41
+ await this.binaryStorage.write({ storageKey, body });
42
+ payloadStorageKey = storageKey;
43
+ payloadText = null;
44
+ } else if (payloadJsonBytes > PAYLOAD_OFFLOAD_THRESHOLD_BYTES) {
45
+ const storageKey = `telemetry-artifacts/${artifactId}.json`;
46
+ const body: BinaryBody = Buffer.from(payloadJson!, "utf8");
47
+ await this.binaryStorage.write({ storageKey, body });
48
+ payloadStorageKey = storageKey;
49
+ payloadJson = null;
50
+ }
51
+
22
52
  await this.prisma.telemetryArtifact.create({
23
53
  data: {
24
54
  artifactId,
@@ -32,8 +62,9 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
32
62
  contentType: record.contentType,
33
63
  previewText: record.previewText ?? null,
34
64
  previewJson: record.previewJson !== undefined ? JSON.stringify(record.previewJson) : null,
35
- payloadText: record.payloadText ?? null,
36
- payloadJson: record.payloadJson !== undefined ? JSON.stringify(record.payloadJson) : null,
65
+ payloadText,
66
+ payloadJson,
67
+ payloadStorageKey,
37
68
  bytes: record.bytes ?? null,
38
69
  truncated: record.truncated ?? null,
39
70
  createdAt,
@@ -53,8 +84,9 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
53
84
  contentType: record.contentType,
54
85
  previewText: record.previewText,
55
86
  previewJson: record.previewJson,
56
- payloadText: record.payloadText,
57
- payloadJson: record.payloadJson,
87
+ payloadText: payloadText ?? undefined,
88
+ payloadJson: payloadJson !== null ? JSON.parse(payloadJson) : undefined,
89
+ payloadStorageKey: payloadStorageKey ?? undefined,
58
90
  bytes: record.bytes,
59
91
  truncated: record.truncated,
60
92
  createdAt,
@@ -82,6 +114,7 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
82
114
  previewJson: this.parseJson(row.previewJson),
83
115
  payloadText: row.payloadText ?? undefined,
84
116
  payloadJson: this.parseJson(row.payloadJson),
117
+ payloadStorageKey: row.payloadStorageKey ?? undefined,
85
118
  bytes: row.bytes ?? undefined,
86
119
  truncated: row.truncated ?? undefined,
87
120
  createdAt: row.createdAt,
@@ -90,7 +123,7 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
90
123
  }));
91
124
  }
92
125
 
93
- async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
126
+ async pruneExpired(args: TelemetryPruneArgs): Promise<{ count: number; storageKeys: ReadonlyArray<string> }> {
94
127
  const rows = await this.prisma.telemetryArtifact.findMany({
95
128
  where: {
96
129
  retentionExpiresAt: {
@@ -99,13 +132,15 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
99
132
  },
100
133
  select: {
101
134
  artifactId: true,
135
+ payloadStorageKey: true,
102
136
  },
103
137
  orderBy: [{ retentionExpiresAt: "asc" }, { artifactId: "asc" }],
104
138
  ...(args.limit ? { take: args.limit } : {}),
105
139
  });
106
140
  if (rows.length === 0) {
107
- return 0;
141
+ return { count: 0, storageKeys: [] };
108
142
  }
143
+ const storageKeys = rows.flatMap((row) => (row.payloadStorageKey ? [row.payloadStorageKey] : []));
109
144
  const result = await this.prisma.telemetryArtifact.deleteMany({
110
145
  where: {
111
146
  artifactId: {
@@ -113,7 +148,7 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
113
148
  },
114
149
  },
115
150
  });
116
- return result.count;
151
+ return { count: result.count, storageKeys };
117
152
  }
118
153
 
119
154
  private parseJson(value: string | null): unknown {