@codemation/host 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +483 -0
  2. package/dist/{ApiPaths-CLTHphYZ.js → ApiPaths-Dv1dcHu_.js} +4 -4
  3. package/dist/ApiPaths-Dv1dcHu_.js.map +1 -0
  4. package/dist/{AppConfigFactory-YnveXE9k.d.ts → AppConfigFactory-BT0y0LVC.d.ts} +8490 -5548
  5. package/dist/{AppConfigFactory-C6q-CSKb.js → AppConfigFactory-Cx4qQvRk.js} +112 -52
  6. package/dist/AppConfigFactory-Cx4qQvRk.js.map +1 -0
  7. package/dist/{AppContainerFactory-qaqc-R1D.js → AppContainerFactory-DRTjG7nG.js} +7298 -1732
  8. package/dist/AppContainerFactory-DRTjG7nG.js.map +1 -0
  9. package/dist/{CodemationAppContext-DRu1Dpri.d.ts → CodemationAppContext-CGFYVcSb.d.ts} +14 -4
  10. package/dist/{CodemationAuthoring.types-DZl-sJaM.js → CodemationAuthoring.types-BteaR3Dc.js} +19 -6
  11. package/dist/CodemationAuthoring.types-BteaR3Dc.js.map +1 -0
  12. package/dist/{CodemationAuthoring.types-fBRppnmi.d.ts → CodemationAuthoring.types-DiKKogum.d.ts} +30 -5
  13. package/dist/{CodemationConfigNormalizer-DVko3cVN.d.ts → CodemationConfigNormalizer-48f-T66P.d.ts} +3 -3
  14. package/dist/{CodemationConsumerConfigLoader-BeAUS144.js → CodemationConsumerConfigLoader-By-6tuGc.js} +81 -10
  15. package/dist/CodemationConsumerConfigLoader-By-6tuGc.js.map +1 -0
  16. package/dist/{CodemationConsumerConfigLoader-DJWr86f-.d.ts → CodemationConsumerConfigLoader-_PIYqwVx.d.ts} +18 -2
  17. package/dist/{CodemationPluginListMerger-B-W5Fa_X.js → CodemationPluginListMerger-D1B1IEbt.js} +1 -1
  18. package/dist/{CodemationPluginListMerger-B-W5Fa_X.js.map → CodemationPluginListMerger-D1B1IEbt.js.map} +1 -1
  19. package/dist/{CodemationPluginListMerger-DGc-jfa2.d.ts → CodemationPluginListMerger-DP7djJ9S.d.ts} +151 -19
  20. package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js +97 -0
  21. package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js.map +1 -0
  22. package/dist/{CodemationWhitelabelConfig-CWbcyQqn.d.ts → CodemationWhitelabelConfig-Ca2mCUeC.d.ts} +2 -2
  23. package/dist/{CollectionContracts.types-DdpHft0i.d.ts → CollectionContracts.types-DDyFYT_D.d.ts} +1 -1
  24. package/dist/{CredentialContractsRegistry-DrMIDSw8.d.ts → CredentialContractsRegistry-Bq2bq28t.d.ts} +2 -2
  25. package/dist/{CredentialServices-UfvHB-rN.d.ts → CredentialServices-BLloBztI.d.ts} +65 -20
  26. package/dist/{CredentialServices-CgxwguAv.js → CredentialServices-Dk8yypeL.js} +310 -51
  27. package/dist/CredentialServices-Dk8yypeL.js.map +1 -0
  28. package/dist/InternalHonoApiRouteRegistrar-c7t3KnV_.d.ts +17 -0
  29. package/dist/InternalPingRegistrar-DY3kSfxP.js +221 -0
  30. package/dist/InternalPingRegistrar-DY3kSfxP.js.map +1 -0
  31. package/dist/{ItemsInputNormalizer-C-KHg9Mo.d.ts → ItemsInputNormalizer-_RwIfRIQ.d.ts} +89 -25
  32. package/dist/{LogLevelPolicyFactory-CampWO0l.d.ts → LogLevelPolicyFactory-ewCHLDLn.d.ts} +2 -2
  33. package/dist/{PublicFrontendBootstrap-DzBgwOnG.d.ts → PublicFrontendBootstrap-Cev3qK46.d.ts} +9 -2
  34. package/dist/PublicFrontendBootstrapFactory-Dv04tJ-6.d.ts +82 -0
  35. package/dist/{PublicFrontendBootstrapJsonCodec-Cl_DLRh0.d.ts → PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts} +3 -3
  36. package/dist/{PublicFrontendBootstrapJsonCodec-DzqvA0uo.js → PublicFrontendBootstrapJsonCodec-CegIF_ne.js} +7 -2
  37. package/dist/PublicFrontendBootstrapJsonCodec-CegIF_ne.js.map +1 -0
  38. package/dist/ServerLoggerFactory-Ckk52S3w.js +223 -0
  39. package/dist/ServerLoggerFactory-Ckk52S3w.js.map +1 -0
  40. package/dist/{TelemetryContracts-DbaNomrH.d.ts → TelemetryContracts-BtDx84Cp.d.ts} +13 -4
  41. package/dist/{WorkflowPolicyUiPresentationFactory-DQEY-h_S.d.ts → WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts} +2 -2
  42. package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js} +1 -1
  43. package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js.map → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map} +1 -1
  44. package/dist/{WorkflowViewContracts-CzK2KFuz.d.ts → WorkflowViewContracts-B7aFQcIw.d.ts} +10 -1
  45. package/dist/authoring.d.ts +5 -5
  46. package/dist/authoring.js +1 -1
  47. package/dist/client.d.ts +4 -4
  48. package/dist/client.js +2 -2
  49. package/dist/consumer.d.ts +6 -6
  50. package/dist/consumer.js +2 -2
  51. package/dist/credentials.d.ts +6 -6
  52. package/dist/credentials.js +1 -1
  53. package/dist/devServerSidecar.d.ts +2 -2
  54. package/dist/devServerSidecar.js +1 -94
  55. package/dist/devServerSidecar.js.map +1 -1
  56. package/dist/dto.d.ts +6 -6
  57. package/dist/{index-BbBk26m0.d.ts → index-DilAYwnH.d.ts} +49 -3
  58. package/dist/index.d.ts +141 -21
  59. package/dist/index.js +109 -14
  60. package/dist/index.js.map +1 -0
  61. package/dist/mapping.d.ts +2 -2
  62. package/dist/mapping.js +1 -1
  63. package/dist/nextServer.d.ts +42 -113
  64. package/dist/nextServer.js +9 -7
  65. package/dist/pairing.d.ts +93 -0
  66. package/dist/pairing.js +5 -0
  67. package/dist/pairing.types-snfZ_OzB.d.ts +19 -0
  68. package/dist/persistenceServer-B71RGvSj.d.ts +30 -0
  69. package/dist/{persistenceServer-CmsIKnO9.js → persistenceServer-C-hH4z6l.js} +2 -2
  70. package/dist/{persistenceServer-CmsIKnO9.js.map → persistenceServer-C-hH4z6l.js.map} +1 -1
  71. package/dist/persistenceServer.d.ts +8 -8
  72. package/dist/persistenceServer.js +3 -3
  73. package/dist/{server-MUNGsBYK.d.ts → server-09PKasWR.d.ts} +21 -6
  74. package/dist/{server-CJFfY67o.js → server-vtRCPgRJ.js} +7 -6
  75. package/dist/{server-CJFfY67o.js.map → server-vtRCPgRJ.js.map} +1 -1
  76. package/dist/server.d.ts +14 -14
  77. package/dist/server.js +13 -11
  78. package/package.json +47 -58
  79. package/prisma/migrations/20260519000000_workflow_audit_log/migration.sql +23 -0
  80. package/prisma/migrations/20260519100000_storage_growth_fixes/migration.sql +61 -0
  81. package/prisma/migrations.sqlite/20260519000000_workflow_audit_log/migration.sql +21 -0
  82. package/prisma/migrations.sqlite/20260519100000_storage_growth_fixes/migration.sql +29 -0
  83. package/prisma/schema.postgresql.prisma +55 -17
  84. package/prisma/schema.sqlite.prisma +55 -17
  85. package/prisma-generated/prisma-postgresql-client/edge.js +33 -5
  86. package/prisma-generated/prisma-postgresql-client/index-browser.js +29 -1
  87. package/prisma-generated/prisma-postgresql-client/index.d.ts +8933 -5716
  88. package/prisma-generated/prisma-postgresql-client/index.js +33 -5
  89. package/prisma-generated/prisma-postgresql-client/package.json +1 -1
  90. package/prisma-generated/prisma-postgresql-client/schema.prisma +38 -0
  91. package/prisma-generated/prisma-sqlite-client/edge.js +33 -5
  92. package/prisma-generated/prisma-sqlite-client/index-browser.js +29 -1
  93. package/prisma-generated/prisma-sqlite-client/index.d.ts +8925 -5713
  94. package/prisma-generated/prisma-sqlite-client/index.js +33 -5
  95. package/prisma-generated/prisma-sqlite-client/package.json +1 -1
  96. package/prisma-generated/prisma-sqlite-client/schema.prisma +38 -0
  97. package/scripts/check-collections.mjs +18 -0
  98. package/scripts/generate-prisma-clients.mjs +20 -11
  99. package/src/application/WorkflowAuditLogPruneScheduler.ts +96 -0
  100. package/src/application/auth/AuthenticatedPrincipal.ts +4 -0
  101. package/src/application/commands/StartWorkflowRunCommandHandler.ts +4 -0
  102. package/src/application/contracts/WorkflowViewContracts.ts +6 -0
  103. package/src/application/contracts/WorkflowWebsocketMessage.ts +3 -1
  104. package/src/application/mapping/WorkflowDefinitionMapper.ts +40 -1
  105. package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +7 -1
  106. package/src/application/telemetry/OtelExecutionTelemetry.types.ts +5 -0
  107. package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +4 -0
  108. package/src/application/telemetry/StoredTelemetrySpanScope.ts +6 -2
  109. package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +27 -17
  110. package/src/application/telemetry/TelemetrySpanPublisher.ts +11 -0
  111. package/src/application/websocket/TelemetrySpanWebsocketRelay.ts +31 -0
  112. package/src/applicationTokens.ts +20 -1
  113. package/src/audit/IAuditEmitter.ts +32 -0
  114. package/src/audit/PrismaWorkflowAuditLogRepository.ts +34 -0
  115. package/src/audit/WorkflowAuditLogWriter.ts +125 -0
  116. package/src/auth/managed/ManagedAuthConfig.ts +29 -0
  117. package/src/auth/managed/ManagedAuthMiddleware.ts +52 -0
  118. package/src/auth/managed/ManagedCorsMiddleware.ts +43 -0
  119. package/src/auth/managed/ManagedModeBootGuard.ts +27 -0
  120. package/src/auth/managed/index.ts +5 -0
  121. package/src/bootstrap/AppContainerFactory.ts +295 -29
  122. package/src/bootstrap/AppContainerLifecycle.ts +31 -0
  123. package/src/bootstrap/perf/BootTimer.ts +168 -0
  124. package/src/bootstrap/runtime/AppConfigFactory.ts +21 -65
  125. package/src/bootstrap/runtime/FrontendRuntime.ts +4 -1
  126. package/src/bootstrap/runtime/HeadlessApiRuntime.ts +47 -0
  127. package/src/bootstrap/runtime/WorkerRuntime.ts +2 -1
  128. package/src/credentials/BrokerClient.ts +49 -0
  129. package/src/credentials/BrokerRefreshError.ts +12 -0
  130. package/src/credentials/BrokerRefreshInvalidGrantError.ts +13 -0
  131. package/src/credentials/ControlPlaneCatalogFetcher.ts +261 -0
  132. package/src/credentials/CredentialOAuth2MaterialReader.ts +136 -0
  133. package/src/credentials/InternalCredentialsListRegistrar.ts +48 -0
  134. package/src/credentials/InternalCredentialsPushRegistrar.ts +125 -0
  135. package/src/credentials/LocalOAuthFlowExecutor.ts +316 -0
  136. package/src/credentials/ManagedOAuthFlowExecutor.ts +94 -0
  137. package/src/credentials/ManagedOAuthRefreshInvalidGrantError.ts +13 -0
  138. package/src/credentials/catalogTypes.ts +4 -0
  139. package/src/credentials/refresh/CredentialDisconnectedError.ts +11 -0
  140. package/src/domain/credentials/CredentialBindingService.ts +54 -2
  141. package/src/domain/credentials/CredentialKeyRotatedError.ts +22 -0
  142. package/src/domain/credentials/CredentialSecretCipher.ts +68 -6
  143. package/src/domain/credentials/CredentialTypeRegistryImpl.ts +117 -10
  144. package/src/domain/credentials/OAuth2RedirectUriResolver.ts +79 -0
  145. package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +14 -5
  146. package/src/domain/telemetry/TelemetryContracts.ts +7 -1
  147. package/src/domain/workflows/WorkflowActivationPreflight.ts +24 -1
  148. package/src/domain/workflows/WorkflowActivationPreflightRules.ts +40 -1
  149. package/src/index.ts +9 -0
  150. package/src/infrastructure/binary/LocalFilesystemBinaryStorageRegistry.ts +29 -1
  151. package/src/infrastructure/binary/S3BinaryStorage.ts +169 -0
  152. package/src/infrastructure/binary/S3BinaryStorageConfig.ts +17 -0
  153. package/src/infrastructure/config/CodemationPluginRegistrar.ts +3 -1
  154. package/src/infrastructure/persistence/CodemationDatabaseUrlParser.ts +41 -0
  155. package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +8 -3
  156. package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +21 -13
  157. package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +43 -8
  158. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +26 -3
  159. package/src/infrastructure/persistence/PrismaWorkflowSnapshotRepository.ts +48 -0
  160. package/src/mcp/AgentMcpIntegrationImpl.ts +344 -0
  161. package/src/mcp/McpClientFactory.ts +29 -0
  162. package/src/mcp/McpConnectionPool.ts +184 -0
  163. package/src/mcp/McpConnectionPool.types.ts +12 -0
  164. package/src/mcp/McpServerCatalog.ts +104 -0
  165. package/src/mcp/index.ts +5 -0
  166. package/src/pairing/HmacRequestSigner.ts +32 -0
  167. package/src/pairing/IncomingHmacVerifier.ts +82 -0
  168. package/src/pairing/InternalHmacAuthMiddleware.ts +33 -0
  169. package/src/pairing/InternalPingRegistrar.ts +25 -0
  170. package/src/pairing/PairedFetch.ts +33 -0
  171. package/src/pairing/PairingConfigFactory.ts +35 -0
  172. package/src/pairing/PairingConfigToken.ts +6 -0
  173. package/src/pairing/index.ts +14 -0
  174. package/src/pairing/pairing.types.ts +18 -0
  175. package/src/pairing.ts +17 -0
  176. package/src/persistenceServer.ts +1 -0
  177. package/src/presentation/config/AppConfig.ts +7 -1
  178. package/src/presentation/config/CodemationAuthConfig.ts +1 -1
  179. package/src/presentation/config/CodemationAuthoring.types.ts +60 -5
  180. package/src/presentation/config/CodemationConfig.ts +9 -0
  181. package/src/presentation/config/CodemationConfigNormalizer.ts +39 -1
  182. package/src/presentation/config/CodemationPlugin.ts +2 -1
  183. package/src/presentation/frontend/CodemationFrontendAuthSnapshot.ts +5 -0
  184. package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -1
  185. package/src/presentation/frontend/PublicFrontendBootstrap.ts +2 -0
  186. package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +5 -1
  187. package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +4 -1
  188. package/src/presentation/http/ApiPaths.ts +4 -4
  189. package/src/presentation/http/HeadlessHttpServerFactory.ts +56 -0
  190. package/src/presentation/http/ServerHttpErrorResponseFactory.ts +39 -2
  191. package/src/presentation/http/hono/CodemationHonoApiAppFactory.ts +33 -8
  192. package/src/presentation/http/hono/InternalHonoApiRouteRegistrar.ts +12 -0
  193. package/src/presentation/http/hono/registrars/ManagedMeHonoApiRouteRegistrar.ts +35 -0
  194. package/src/presentation/http/hono/registrars/OAuth2HonoApiRouteRegistrar.ts +2 -2
  195. package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +28 -0
  196. package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +98 -41
  197. package/src/presentation/server/CodemationConsumerConfigLoader.ts +59 -7
  198. package/src/presentation/server/CodemationPluginDiscovery.ts +5 -0
  199. package/src/presentation/server/WorkflowDefinitionExportsResolver.ts +18 -0
  200. package/src/presentation/server/WorkflowModulePathFinder.ts +12 -1
  201. package/src/presentation/websocket/ManagedWebsocketAuthenticator.ts +50 -0
  202. package/src/presentation/websocket/WebsocketAuthenticator.types.ts +12 -0
  203. package/src/presentation/websocket/WorkflowWebsocketServer.ts +24 -3
  204. package/src/presentation/websocket/WorkflowWebsocketServerFactory.ts +16 -0
  205. package/src/process/ExecaProcessRunner.ts +41 -0
  206. package/src/process/ProcessRunner.types.ts +39 -0
  207. package/src/server.ts +2 -0
  208. package/src/workflows/InternalWorkflowActivationRegistrar.ts +42 -0
  209. package/src/workflows/InternalWorkflowDetailRegistrar.ts +33 -0
  210. package/src/workflows/InternalWorkflowTestRunRegistrar.ts +91 -0
  211. package/src/workflows/InternalWorkflowsListRegistrar.ts +28 -0
  212. package/src/workflows/discovery/WorkflowDirectoryDiscoverer.ts +79 -0
  213. package/tsconfig.json +2 -0
  214. package/vitest.shared.ts +5 -0
  215. package/dist/ApiPaths-CLTHphYZ.js.map +0 -1
  216. package/dist/AppConfigFactory-C6q-CSKb.js.map +0 -1
  217. package/dist/AppContainerFactory-qaqc-R1D.js.map +0 -1
  218. package/dist/CodemationAuthoring.types-DZl-sJaM.js.map +0 -1
  219. package/dist/CodemationConsumerConfigLoader-BeAUS144.js.map +0 -1
  220. package/dist/CredentialServices-CgxwguAv.js.map +0 -1
  221. package/dist/PublicFrontendBootstrapFactory-Cb2pLmDd.d.ts +0 -45
  222. package/dist/PublicFrontendBootstrapJsonCodec-DzqvA0uo.js.map +0 -1
  223. package/dist/ServerLoggerFactory-BKSIh9Xv.js +0 -98
  224. package/dist/ServerLoggerFactory-BKSIh9Xv.js.map +0 -1
  225. package/dist/persistenceServer-vtJAGDat.d.ts +0 -9
  226. package/src/domain/credentials/OAuth2ConnectServiceFactory.ts +0 -411
@@ -1,29 +1,136 @@
1
1
  import type { CredentialTypeDefinition, CredentialTypeId, CredentialTypeRegistry } from "@codemation/core";
2
2
 
3
- import { injectable } from "@codemation/core";
3
+ import { inject, injectable } from "@codemation/core";
4
4
 
5
- import type { AnyCredentialType, CredentialType } from "./CredentialServices";
5
+ import { ApplicationTokens } from "../../applicationTokens";
6
+ import type { LoggerFactory } from "../../application/logging/Logger";
7
+ import type { AnyCredentialType } from "./CredentialServices";
8
+
9
+ export type CredentialTypeSource = "plugin" | "config" | "controlPlane";
10
+
11
+ const SOURCE_PRIORITY: Record<CredentialTypeSource, number> = {
12
+ plugin: 0,
13
+ config: 1,
14
+ controlPlane: 2,
15
+ };
16
+
17
+ type RegistryEntry = Readonly<{
18
+ type: AnyCredentialType;
19
+ source: CredentialTypeSource;
20
+ }>;
6
21
 
7
22
  @injectable()
8
23
  export class CredentialTypeRegistryImpl implements CredentialTypeRegistry {
9
- private readonly credentialTypesById = new Map<CredentialTypeId, AnyCredentialType>();
24
+ private readonly entries = new Map<CredentialTypeId, RegistryEntry>();
25
+ private readonly bySource = new Map<CredentialTypeSource, Set<CredentialTypeId>>();
10
26
 
11
- register(type: CredentialType<any, any, unknown>): void {
12
- if (this.credentialTypesById.has(type.definition.typeId)) {
13
- throw new Error(`Credential type already registered: ${type.definition.typeId}`);
27
+ constructor(@inject(ApplicationTokens.LoggerFactory) private readonly loggers: LoggerFactory) {}
28
+
29
+ merge(source: CredentialTypeSource, types: ReadonlyArray<AnyCredentialType>): void {
30
+ const logger = this.loggers.create("CredentialTypeRegistryImpl");
31
+ for (const type of types) {
32
+ this.insert(source, type, logger);
33
+ }
34
+ }
35
+
36
+ mergeDefinitions(source: CredentialTypeSource, definitions: ReadonlyArray<CredentialTypeDefinition>): void {
37
+ const logger = this.loggers.create("CredentialTypeRegistryImpl");
38
+ for (const definition of definitions) {
39
+ const existing = this.entries.get(definition.typeId);
40
+ const sourcePriority = SOURCE_PRIORITY[source];
41
+ if (existing) {
42
+ if (sourcePriority < SOURCE_PRIORITY[existing.source]) {
43
+ logger.warn(
44
+ `CredentialTypeRegistryImpl: id collision — lower-priority source "${source}" ignored for typeId "${definition.typeId}" (current source: "${existing.source}")`,
45
+ );
46
+ continue;
47
+ }
48
+ if (sourcePriority > SOURCE_PRIORITY[existing.source]) {
49
+ logger.warn(
50
+ `CredentialTypeRegistryImpl: typeId "${definition.typeId}" shadowed — "${existing.source}" overridden by higher-priority source "${source}"`,
51
+ );
52
+ this.bySource.get(existing.source)?.delete(definition.typeId);
53
+ }
54
+ const nextType: AnyCredentialType =
55
+ sourcePriority === SOURCE_PRIORITY[existing.source]
56
+ ? { ...existing.type, definition }
57
+ : { definition, createSession: this.createUnsupportedSessionFactory(definition.typeId, source), test: this.createUnsupportedHealthTester(definition.typeId, source) };
58
+ this.recordEntry(definition.typeId, { type: nextType, source });
59
+ continue;
60
+ }
61
+ const stubType: AnyCredentialType = {
62
+ definition,
63
+ createSession: this.createUnsupportedSessionFactory(definition.typeId, source),
64
+ test: this.createUnsupportedHealthTester(definition.typeId, source),
65
+ };
66
+ this.recordEntry(definition.typeId, { type: stubType, source });
67
+ }
68
+ }
69
+
70
+ clear(source: CredentialTypeSource): void {
71
+ const ids = this.bySource.get(source);
72
+ if (!ids) {
73
+ return;
74
+ }
75
+ for (const id of ids) {
76
+ this.entries.delete(id);
14
77
  }
15
- this.credentialTypesById.set(type.definition.typeId, type);
78
+ this.bySource.delete(source);
16
79
  }
17
80
 
18
81
  listTypes(): ReadonlyArray<CredentialTypeDefinition> {
19
- return [...this.credentialTypesById.values()].map((entry) => entry.definition);
82
+ return [...this.entries.values()].map((entry) => entry.type.definition);
20
83
  }
21
84
 
22
85
  getType(typeId: CredentialTypeId): CredentialTypeDefinition | undefined {
23
- return this.credentialTypesById.get(typeId)?.definition;
86
+ return this.entries.get(typeId)?.type.definition;
24
87
  }
25
88
 
26
89
  getCredentialType(typeId: CredentialTypeId): AnyCredentialType | undefined {
27
- return this.credentialTypesById.get(typeId);
90
+ return this.entries.get(typeId)?.type;
91
+ }
92
+
93
+ private insert(source: CredentialTypeSource, type: AnyCredentialType, logger: ReturnType<LoggerFactory["create"]>): void {
94
+ const typeId = type.definition.typeId;
95
+ const existing = this.entries.get(typeId);
96
+ const sourcePriority = SOURCE_PRIORITY[source];
97
+ if (existing) {
98
+ if (sourcePriority < SOURCE_PRIORITY[existing.source]) {
99
+ logger.warn(
100
+ `CredentialTypeRegistryImpl: id collision — lower-priority source "${source}" ignored for typeId "${typeId}" (current source: "${existing.source}")`,
101
+ );
102
+ return;
103
+ }
104
+ if (sourcePriority > SOURCE_PRIORITY[existing.source]) {
105
+ logger.warn(
106
+ `CredentialTypeRegistryImpl: typeId "${typeId}" shadowed — "${existing.source}" overridden by higher-priority source "${source}"`,
107
+ );
108
+ this.bySource.get(existing.source)?.delete(typeId);
109
+ }
110
+ }
111
+ this.recordEntry(typeId, { type, source });
112
+ }
113
+
114
+ private recordEntry(typeId: CredentialTypeId, entry: RegistryEntry): void {
115
+ this.entries.set(typeId, entry);
116
+ if (!this.bySource.has(entry.source)) {
117
+ this.bySource.set(entry.source, new Set());
118
+ }
119
+ this.bySource.get(entry.source)!.add(typeId);
120
+ }
121
+
122
+ private createUnsupportedSessionFactory(typeId: CredentialTypeId, source: CredentialTypeSource): AnyCredentialType["createSession"] {
123
+ return async () => {
124
+ throw new Error(
125
+ `Credential type "${typeId}" (source "${source}") was registered with definition only — no createSession implementation is available in this runtime.`,
126
+ );
127
+ };
128
+ }
129
+
130
+ private createUnsupportedHealthTester(typeId: CredentialTypeId, source: CredentialTypeSource): AnyCredentialType["test"] {
131
+ return async () => ({
132
+ status: "unknown" as const,
133
+ message: `Credential type "${typeId}" (source "${source}") has no local test implementation.`,
134
+ });
28
135
  }
29
136
  }
@@ -0,0 +1,79 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import { ApplicationRequestError } from "../../application/ApplicationRequestError";
3
+ import { ApplicationTokens } from "../../applicationTokens";
4
+ import type { AppConfig } from "../../presentation/config/AppConfig";
5
+
6
+ /**
7
+ * Resolves the canonical OAuth2 redirect URI from the public base URL or request origin.
8
+ * The redirect URI always points to `/api/oauth2/callback`, which is the URL operators
9
+ * register with their OAuth provider.
10
+ */
11
+ @injectable()
12
+ export class OAuth2RedirectUriResolver {
13
+ constructor(
14
+ @inject(ApplicationTokens.AppConfig)
15
+ private readonly appConfig: AppConfig,
16
+ ) {}
17
+
18
+ resolve(requestOrigin: string): string {
19
+ const rawBase = this.appConfig.env.CODEMATION_PUBLIC_BASE_URL?.trim() || requestOrigin.trim();
20
+ if (!rawBase) {
21
+ throw new Error("Unable to resolve the public base URL for OAuth2 redirect URI generation.");
22
+ }
23
+ const baseUrl = this.ensureAbsoluteUrl(rawBase);
24
+ try {
25
+ const callback = new URL("/api/oauth2/callback", this.normalizeBaseUrl(baseUrl));
26
+ // Several OAuth2 providers (notably Azure AD / Microsoft) reject raw loopback IPs in
27
+ // redirect URIs and only allow the `localhost` hostname. 127.0.0.1 / [::1] are equivalent
28
+ // to localhost by definition, so rewriting is lossless.
29
+ const loopbackHostnames = new Set(["127.0.0.1", "[::1]"]);
30
+ if (loopbackHostnames.has(callback.hostname)) {
31
+ callback.hostname = "localhost";
32
+ }
33
+ return callback.toString();
34
+ } catch {
35
+ throw new ApplicationRequestError(
36
+ 500,
37
+ `Invalid public base URL for OAuth2 redirect URI generation: "${rawBase}". Use a full URL (e.g. http://localhost:3000) for CODEMATION_PUBLIC_BASE_URL or ensure the request has a valid Host / forwarded headers.`,
38
+ );
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Ensures the base URL has an http/https scheme. Comma-separated values (proxy chains) use
44
+ * the first segment only.
45
+ */
46
+ private ensureAbsoluteUrl(raw: string): string {
47
+ const segments = raw
48
+ .split(",")
49
+ .map((s) => s.trim())
50
+ .filter((s) => s.length > 0);
51
+ let candidate = segments[0] ?? raw.trim();
52
+ if (!candidate) {
53
+ throw new Error("Unable to resolve the public base URL for OAuth2 redirect URI generation.");
54
+ }
55
+ if (!/^https?:\/\//i.test(candidate)) {
56
+ candidate = `http://${candidate}`;
57
+ }
58
+ let parsed: URL;
59
+ try {
60
+ parsed = new URL(candidate);
61
+ } catch {
62
+ throw new ApplicationRequestError(
63
+ 500,
64
+ `Invalid public base URL for OAuth2 redirect URI generation: "${raw}". Use a single full URL (e.g. http://localhost:3000) for CODEMATION_PUBLIC_BASE_URL.`,
65
+ );
66
+ }
67
+ if (parsed.hostname === "http" || parsed.hostname === "https") {
68
+ throw new ApplicationRequestError(
69
+ 500,
70
+ `Invalid OAuth2 public base URL (hostname "${parsed.hostname}"). Set CODEMATION_PUBLIC_BASE_URL to one full URL with a real host, e.g. http://localhost:3000 — not "http,http" or other typos.`,
71
+ );
72
+ }
73
+ return candidate;
74
+ }
75
+
76
+ private normalizeBaseUrl(baseUrl: string): string {
77
+ return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
78
+ }
79
+ }
@@ -4,9 +4,10 @@ import {
4
4
  AgentConnectionNodeCollector,
5
5
  type AgentConnectionNodeDescriptor,
6
6
  ConnectionNodeIdFactory,
7
+ inject,
8
+ injectable,
7
9
  } from "@codemation/core";
8
-
9
- import { injectable } from "@codemation/core";
10
+ import { McpServerCatalog } from "../../mcp/McpServerCatalog";
10
11
 
11
12
  export type WorkflowCredentialSlotRef = Readonly<{
12
13
  workflowId: string;
@@ -20,6 +21,10 @@ export type WorkflowCredentialSlotRef = Readonly<{
20
21
  */
21
22
  @injectable()
22
23
  export class WorkflowCredentialNodeResolver {
24
+ constructor(
25
+ @inject(McpServerCatalog)
26
+ private readonly mcpCatalog?: McpServerCatalog,
27
+ ) {}
23
28
  /**
24
29
  * Human-readable label for credential errors (workflow node name or agent › attachment).
25
30
  */
@@ -102,7 +107,9 @@ export class WorkflowCredentialNodeResolver {
102
107
  agentConfig: Parameters<typeof AgentConnectionNodeCollector.collect>[1],
103
108
  slotsByKey: Map<string, WorkflowCredentialSlotRef>,
104
109
  ): void {
105
- for (const entry of AgentConnectionNodeCollector.collect(rootAgentNodeId, agentConfig)) {
110
+ const mcpResolver = this.mcpCatalog ? (id: string) => this.mcpCatalog!.get(id) : undefined;
111
+ const descriptors = AgentConnectionNodeCollector.collect(rootAgentNodeId, agentConfig, mcpResolver);
112
+ for (const entry of descriptors) {
106
113
  this.addSlotsForRequirements(
107
114
  workflowId,
108
115
  entry.nodeId,
@@ -147,15 +154,17 @@ export class WorkflowCredentialNodeResolver {
147
154
  | undefined {
148
155
  if (
149
156
  !ConnectionNodeIdFactory.isLanguageModelConnectionNodeId(nodeId) &&
150
- !ConnectionNodeIdFactory.isToolConnectionNodeId(nodeId)
157
+ !ConnectionNodeIdFactory.isToolConnectionNodeId(nodeId) &&
158
+ !ConnectionNodeIdFactory.isMcpConnectionNodeId(nodeId)
151
159
  ) {
152
160
  return undefined;
153
161
  }
162
+ const mcpResolver = this.mcpCatalog ? (id: string) => this.mcpCatalog!.get(id) : undefined;
154
163
  for (const node of workflow.nodes) {
155
164
  if (!AgentConfigInspector.isAgentNodeConfig(node.config)) {
156
165
  continue;
157
166
  }
158
- const entries = AgentConnectionNodeCollector.collect(node.id, node.config);
167
+ const entries = AgentConnectionNodeCollector.collect(node.id, node.config, mcpResolver);
159
168
  const entriesById = new Map(entries.map((entry) => [entry.nodeId, entry]));
160
169
  const entry = entriesById.get(nodeId);
161
170
  if (!entry) {
@@ -86,6 +86,8 @@ export interface TelemetryArtifactRecord {
86
86
  readonly previewJson?: unknown;
87
87
  readonly payloadText?: string;
88
88
  readonly payloadJson?: unknown;
89
+ /** Set when the payload was offloaded to BinaryStorage (byteLength > 64 KB). */
90
+ readonly payloadStorageKey?: string;
89
91
  readonly bytes?: number;
90
92
  readonly truncated?: boolean;
91
93
  readonly createdAt: string;
@@ -195,7 +197,11 @@ export interface TelemetrySpanStore {
195
197
  export interface TelemetryArtifactStore {
196
198
  save(record: TelemetryArtifactWrite): Promise<TelemetryArtifactRecord>;
197
199
  listByTraceId(traceId: string): Promise<ReadonlyArray<TelemetryArtifactRecord>>;
198
- pruneExpired(args: TelemetryPruneArgs): Promise<number>;
200
+ /**
201
+ * Deletes expired artifacts. Returns the count of deleted rows and any
202
+ * `payloadStorageKey` references that callers must clean up from BinaryStorage.
203
+ */
204
+ pruneExpired(args: TelemetryPruneArgs): Promise<{ count: number; storageKeys: ReadonlyArray<string> }>;
199
205
  }
200
206
 
201
207
  export interface TelemetryMetricPointStore {
@@ -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,11 +6,16 @@ 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";
12
14
  export { WorkerRuntime } from "./bootstrap/runtime/WorkerRuntime";
13
15
  export { AppConfigFactory } from "./bootstrap/runtime/AppConfigFactory";
16
+ export { HeadlessApiRuntime } from "./bootstrap/runtime/HeadlessApiRuntime";
17
+ export { WorkflowWebsocketServerFactory } from "./presentation/websocket/WorkflowWebsocketServerFactory";
18
+ export { HeadlessHttpServerFactory } from "./presentation/http/HeadlessHttpServerFactory";
14
19
  export { ApplicationTokens } from "./applicationTokens";
15
20
  export { workflow } from "@codemation/core-nodes";
16
21
  export { CodemationBootstrapRequest } from "./bootstrap/CodemationBootstrapRequest";
@@ -56,6 +61,10 @@ export { InsertCollectionRowCommand } from "./application/collections/InsertColl
56
61
  export { UpdateCollectionRowCommand } from "./application/collections/UpdateCollectionRowCommand";
57
62
  export { DeleteCollectionRowCommand } from "./application/collections/DeleteCollectionRowCommand";
58
63
  export { SyncCollectionsCommand } from "./application/collections/SyncCollectionsCommand";
64
+ export { StartWorkflowRunCommand } from "./application/commands/StartWorkflowRunCommand";
65
+ export type { RunCommandResult } from "./application/contracts/RunContracts";
66
+ export { ApplicationRequestError } from "./application/ApplicationRequestError";
67
+ export { GetRunStateQuery } from "./application/queries/GetRunStateQuery";
59
68
 
60
69
  export type {
61
70
  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
+ });