@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.
- package/CHANGELOG.md +483 -0
- package/dist/{ApiPaths-CLTHphYZ.js → ApiPaths-Dv1dcHu_.js} +4 -4
- package/dist/ApiPaths-Dv1dcHu_.js.map +1 -0
- package/dist/{AppConfigFactory-YnveXE9k.d.ts → AppConfigFactory-BT0y0LVC.d.ts} +8490 -5548
- package/dist/{AppConfigFactory-C6q-CSKb.js → AppConfigFactory-Cx4qQvRk.js} +112 -52
- package/dist/AppConfigFactory-Cx4qQvRk.js.map +1 -0
- package/dist/{AppContainerFactory-qaqc-R1D.js → AppContainerFactory-DRTjG7nG.js} +7298 -1732
- package/dist/AppContainerFactory-DRTjG7nG.js.map +1 -0
- package/dist/{CodemationAppContext-DRu1Dpri.d.ts → CodemationAppContext-CGFYVcSb.d.ts} +14 -4
- package/dist/{CodemationAuthoring.types-DZl-sJaM.js → CodemationAuthoring.types-BteaR3Dc.js} +19 -6
- package/dist/CodemationAuthoring.types-BteaR3Dc.js.map +1 -0
- package/dist/{CodemationAuthoring.types-fBRppnmi.d.ts → CodemationAuthoring.types-DiKKogum.d.ts} +30 -5
- package/dist/{CodemationConfigNormalizer-DVko3cVN.d.ts → CodemationConfigNormalizer-48f-T66P.d.ts} +3 -3
- package/dist/{CodemationConsumerConfigLoader-BeAUS144.js → CodemationConsumerConfigLoader-By-6tuGc.js} +81 -10
- package/dist/CodemationConsumerConfigLoader-By-6tuGc.js.map +1 -0
- package/dist/{CodemationConsumerConfigLoader-DJWr86f-.d.ts → CodemationConsumerConfigLoader-_PIYqwVx.d.ts} +18 -2
- package/dist/{CodemationPluginListMerger-B-W5Fa_X.js → CodemationPluginListMerger-D1B1IEbt.js} +1 -1
- package/dist/{CodemationPluginListMerger-B-W5Fa_X.js.map → CodemationPluginListMerger-D1B1IEbt.js.map} +1 -1
- package/dist/{CodemationPluginListMerger-DGc-jfa2.d.ts → CodemationPluginListMerger-DP7djJ9S.d.ts} +151 -19
- package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js +97 -0
- package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js.map +1 -0
- package/dist/{CodemationWhitelabelConfig-CWbcyQqn.d.ts → CodemationWhitelabelConfig-Ca2mCUeC.d.ts} +2 -2
- package/dist/{CollectionContracts.types-DdpHft0i.d.ts → CollectionContracts.types-DDyFYT_D.d.ts} +1 -1
- package/dist/{CredentialContractsRegistry-DrMIDSw8.d.ts → CredentialContractsRegistry-Bq2bq28t.d.ts} +2 -2
- package/dist/{CredentialServices-UfvHB-rN.d.ts → CredentialServices-BLloBztI.d.ts} +65 -20
- package/dist/{CredentialServices-CgxwguAv.js → CredentialServices-Dk8yypeL.js} +310 -51
- package/dist/CredentialServices-Dk8yypeL.js.map +1 -0
- package/dist/InternalHonoApiRouteRegistrar-c7t3KnV_.d.ts +17 -0
- package/dist/InternalPingRegistrar-DY3kSfxP.js +221 -0
- package/dist/InternalPingRegistrar-DY3kSfxP.js.map +1 -0
- package/dist/{ItemsInputNormalizer-C-KHg9Mo.d.ts → ItemsInputNormalizer-_RwIfRIQ.d.ts} +89 -25
- package/dist/{LogLevelPolicyFactory-CampWO0l.d.ts → LogLevelPolicyFactory-ewCHLDLn.d.ts} +2 -2
- package/dist/{PublicFrontendBootstrap-DzBgwOnG.d.ts → PublicFrontendBootstrap-Cev3qK46.d.ts} +9 -2
- package/dist/PublicFrontendBootstrapFactory-Dv04tJ-6.d.ts +82 -0
- package/dist/{PublicFrontendBootstrapJsonCodec-Cl_DLRh0.d.ts → PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts} +3 -3
- package/dist/{PublicFrontendBootstrapJsonCodec-DzqvA0uo.js → PublicFrontendBootstrapJsonCodec-CegIF_ne.js} +7 -2
- package/dist/PublicFrontendBootstrapJsonCodec-CegIF_ne.js.map +1 -0
- package/dist/ServerLoggerFactory-Ckk52S3w.js +223 -0
- package/dist/ServerLoggerFactory-Ckk52S3w.js.map +1 -0
- package/dist/{TelemetryContracts-DbaNomrH.d.ts → TelemetryContracts-BtDx84Cp.d.ts} +13 -4
- package/dist/{WorkflowPolicyUiPresentationFactory-DQEY-h_S.d.ts → WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts} +2 -2
- package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js} +1 -1
- package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js.map → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map} +1 -1
- package/dist/{WorkflowViewContracts-CzK2KFuz.d.ts → WorkflowViewContracts-B7aFQcIw.d.ts} +10 -1
- package/dist/authoring.d.ts +5 -5
- package/dist/authoring.js +1 -1
- package/dist/client.d.ts +4 -4
- package/dist/client.js +2 -2
- package/dist/consumer.d.ts +6 -6
- package/dist/consumer.js +2 -2
- package/dist/credentials.d.ts +6 -6
- package/dist/credentials.js +1 -1
- package/dist/devServerSidecar.d.ts +2 -2
- package/dist/devServerSidecar.js +1 -94
- package/dist/devServerSidecar.js.map +1 -1
- package/dist/dto.d.ts +6 -6
- package/dist/{index-BbBk26m0.d.ts → index-DilAYwnH.d.ts} +49 -3
- package/dist/index.d.ts +141 -21
- package/dist/index.js +109 -14
- package/dist/index.js.map +1 -0
- package/dist/mapping.d.ts +2 -2
- package/dist/mapping.js +1 -1
- package/dist/nextServer.d.ts +42 -113
- package/dist/nextServer.js +9 -7
- package/dist/pairing.d.ts +93 -0
- package/dist/pairing.js +5 -0
- package/dist/pairing.types-snfZ_OzB.d.ts +19 -0
- package/dist/persistenceServer-B71RGvSj.d.ts +30 -0
- package/dist/{persistenceServer-CmsIKnO9.js → persistenceServer-C-hH4z6l.js} +2 -2
- package/dist/{persistenceServer-CmsIKnO9.js.map → persistenceServer-C-hH4z6l.js.map} +1 -1
- package/dist/persistenceServer.d.ts +8 -8
- package/dist/persistenceServer.js +3 -3
- package/dist/{server-MUNGsBYK.d.ts → server-09PKasWR.d.ts} +21 -6
- package/dist/{server-CJFfY67o.js → server-vtRCPgRJ.js} +7 -6
- package/dist/{server-CJFfY67o.js.map → server-vtRCPgRJ.js.map} +1 -1
- package/dist/server.d.ts +14 -14
- package/dist/server.js +13 -11
- package/package.json +47 -58
- package/prisma/migrations/20260519000000_workflow_audit_log/migration.sql +23 -0
- package/prisma/migrations/20260519100000_storage_growth_fixes/migration.sql +61 -0
- package/prisma/migrations.sqlite/20260519000000_workflow_audit_log/migration.sql +21 -0
- package/prisma/migrations.sqlite/20260519100000_storage_growth_fixes/migration.sql +29 -0
- package/prisma/schema.postgresql.prisma +55 -17
- package/prisma/schema.sqlite.prisma +55 -17
- package/prisma-generated/prisma-postgresql-client/edge.js +33 -5
- package/prisma-generated/prisma-postgresql-client/index-browser.js +29 -1
- package/prisma-generated/prisma-postgresql-client/index.d.ts +8933 -5716
- package/prisma-generated/prisma-postgresql-client/index.js +33 -5
- package/prisma-generated/prisma-postgresql-client/package.json +1 -1
- package/prisma-generated/prisma-postgresql-client/schema.prisma +38 -0
- package/prisma-generated/prisma-sqlite-client/edge.js +33 -5
- package/prisma-generated/prisma-sqlite-client/index-browser.js +29 -1
- package/prisma-generated/prisma-sqlite-client/index.d.ts +8925 -5713
- package/prisma-generated/prisma-sqlite-client/index.js +33 -5
- package/prisma-generated/prisma-sqlite-client/package.json +1 -1
- package/prisma-generated/prisma-sqlite-client/schema.prisma +38 -0
- package/scripts/check-collections.mjs +18 -0
- package/scripts/generate-prisma-clients.mjs +20 -11
- package/src/application/WorkflowAuditLogPruneScheduler.ts +96 -0
- package/src/application/auth/AuthenticatedPrincipal.ts +4 -0
- package/src/application/commands/StartWorkflowRunCommandHandler.ts +4 -0
- package/src/application/contracts/WorkflowViewContracts.ts +6 -0
- package/src/application/contracts/WorkflowWebsocketMessage.ts +3 -1
- package/src/application/mapping/WorkflowDefinitionMapper.ts +40 -1
- package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +7 -1
- package/src/application/telemetry/OtelExecutionTelemetry.types.ts +5 -0
- package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +4 -0
- package/src/application/telemetry/StoredTelemetrySpanScope.ts +6 -2
- package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +27 -17
- package/src/application/telemetry/TelemetrySpanPublisher.ts +11 -0
- package/src/application/websocket/TelemetrySpanWebsocketRelay.ts +31 -0
- package/src/applicationTokens.ts +20 -1
- package/src/audit/IAuditEmitter.ts +32 -0
- package/src/audit/PrismaWorkflowAuditLogRepository.ts +34 -0
- package/src/audit/WorkflowAuditLogWriter.ts +125 -0
- package/src/auth/managed/ManagedAuthConfig.ts +29 -0
- package/src/auth/managed/ManagedAuthMiddleware.ts +52 -0
- package/src/auth/managed/ManagedCorsMiddleware.ts +43 -0
- package/src/auth/managed/ManagedModeBootGuard.ts +27 -0
- package/src/auth/managed/index.ts +5 -0
- package/src/bootstrap/AppContainerFactory.ts +295 -29
- package/src/bootstrap/AppContainerLifecycle.ts +31 -0
- package/src/bootstrap/perf/BootTimer.ts +168 -0
- package/src/bootstrap/runtime/AppConfigFactory.ts +21 -65
- package/src/bootstrap/runtime/FrontendRuntime.ts +4 -1
- package/src/bootstrap/runtime/HeadlessApiRuntime.ts +47 -0
- package/src/bootstrap/runtime/WorkerRuntime.ts +2 -1
- package/src/credentials/BrokerClient.ts +49 -0
- package/src/credentials/BrokerRefreshError.ts +12 -0
- package/src/credentials/BrokerRefreshInvalidGrantError.ts +13 -0
- package/src/credentials/ControlPlaneCatalogFetcher.ts +261 -0
- package/src/credentials/CredentialOAuth2MaterialReader.ts +136 -0
- package/src/credentials/InternalCredentialsListRegistrar.ts +48 -0
- package/src/credentials/InternalCredentialsPushRegistrar.ts +125 -0
- package/src/credentials/LocalOAuthFlowExecutor.ts +316 -0
- package/src/credentials/ManagedOAuthFlowExecutor.ts +94 -0
- package/src/credentials/ManagedOAuthRefreshInvalidGrantError.ts +13 -0
- package/src/credentials/catalogTypes.ts +4 -0
- package/src/credentials/refresh/CredentialDisconnectedError.ts +11 -0
- package/src/domain/credentials/CredentialBindingService.ts +54 -2
- package/src/domain/credentials/CredentialKeyRotatedError.ts +22 -0
- package/src/domain/credentials/CredentialSecretCipher.ts +68 -6
- package/src/domain/credentials/CredentialTypeRegistryImpl.ts +117 -10
- package/src/domain/credentials/OAuth2RedirectUriResolver.ts +79 -0
- package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +14 -5
- package/src/domain/telemetry/TelemetryContracts.ts +7 -1
- package/src/domain/workflows/WorkflowActivationPreflight.ts +24 -1
- package/src/domain/workflows/WorkflowActivationPreflightRules.ts +40 -1
- package/src/index.ts +9 -0
- package/src/infrastructure/binary/LocalFilesystemBinaryStorageRegistry.ts +29 -1
- package/src/infrastructure/binary/S3BinaryStorage.ts +169 -0
- package/src/infrastructure/binary/S3BinaryStorageConfig.ts +17 -0
- package/src/infrastructure/config/CodemationPluginRegistrar.ts +3 -1
- package/src/infrastructure/persistence/CodemationDatabaseUrlParser.ts +41 -0
- package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +8 -3
- package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +21 -13
- package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +43 -8
- package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +26 -3
- package/src/infrastructure/persistence/PrismaWorkflowSnapshotRepository.ts +48 -0
- package/src/mcp/AgentMcpIntegrationImpl.ts +344 -0
- package/src/mcp/McpClientFactory.ts +29 -0
- package/src/mcp/McpConnectionPool.ts +184 -0
- package/src/mcp/McpConnectionPool.types.ts +12 -0
- package/src/mcp/McpServerCatalog.ts +104 -0
- package/src/mcp/index.ts +5 -0
- package/src/pairing/HmacRequestSigner.ts +32 -0
- package/src/pairing/IncomingHmacVerifier.ts +82 -0
- package/src/pairing/InternalHmacAuthMiddleware.ts +33 -0
- package/src/pairing/InternalPingRegistrar.ts +25 -0
- package/src/pairing/PairedFetch.ts +33 -0
- package/src/pairing/PairingConfigFactory.ts +35 -0
- package/src/pairing/PairingConfigToken.ts +6 -0
- package/src/pairing/index.ts +14 -0
- package/src/pairing/pairing.types.ts +18 -0
- package/src/pairing.ts +17 -0
- package/src/persistenceServer.ts +1 -0
- package/src/presentation/config/AppConfig.ts +7 -1
- package/src/presentation/config/CodemationAuthConfig.ts +1 -1
- package/src/presentation/config/CodemationAuthoring.types.ts +60 -5
- package/src/presentation/config/CodemationConfig.ts +9 -0
- package/src/presentation/config/CodemationConfigNormalizer.ts +39 -1
- package/src/presentation/config/CodemationPlugin.ts +2 -1
- package/src/presentation/frontend/CodemationFrontendAuthSnapshot.ts +5 -0
- package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -1
- package/src/presentation/frontend/PublicFrontendBootstrap.ts +2 -0
- package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +5 -1
- package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +4 -1
- package/src/presentation/http/ApiPaths.ts +4 -4
- package/src/presentation/http/HeadlessHttpServerFactory.ts +56 -0
- package/src/presentation/http/ServerHttpErrorResponseFactory.ts +39 -2
- package/src/presentation/http/hono/CodemationHonoApiAppFactory.ts +33 -8
- package/src/presentation/http/hono/InternalHonoApiRouteRegistrar.ts +12 -0
- package/src/presentation/http/hono/registrars/ManagedMeHonoApiRouteRegistrar.ts +35 -0
- package/src/presentation/http/hono/registrars/OAuth2HonoApiRouteRegistrar.ts +2 -2
- package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +28 -0
- package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +98 -41
- package/src/presentation/server/CodemationConsumerConfigLoader.ts +59 -7
- package/src/presentation/server/CodemationPluginDiscovery.ts +5 -0
- package/src/presentation/server/WorkflowDefinitionExportsResolver.ts +18 -0
- package/src/presentation/server/WorkflowModulePathFinder.ts +12 -1
- package/src/presentation/websocket/ManagedWebsocketAuthenticator.ts +50 -0
- package/src/presentation/websocket/WebsocketAuthenticator.types.ts +12 -0
- package/src/presentation/websocket/WorkflowWebsocketServer.ts +24 -3
- package/src/presentation/websocket/WorkflowWebsocketServerFactory.ts +16 -0
- package/src/process/ExecaProcessRunner.ts +41 -0
- package/src/process/ProcessRunner.types.ts +39 -0
- package/src/server.ts +2 -0
- package/src/workflows/InternalWorkflowActivationRegistrar.ts +42 -0
- package/src/workflows/InternalWorkflowDetailRegistrar.ts +33 -0
- package/src/workflows/InternalWorkflowTestRunRegistrar.ts +91 -0
- package/src/workflows/InternalWorkflowsListRegistrar.ts +28 -0
- package/src/workflows/discovery/WorkflowDirectoryDiscoverer.ts +79 -0
- package/tsconfig.json +2 -0
- package/vitest.shared.ts +5 -0
- package/dist/ApiPaths-CLTHphYZ.js.map +0 -1
- package/dist/AppConfigFactory-C6q-CSKb.js.map +0 -1
- package/dist/AppContainerFactory-qaqc-R1D.js.map +0 -1
- package/dist/CodemationAuthoring.types-DZl-sJaM.js.map +0 -1
- package/dist/CodemationConsumerConfigLoader-BeAUS144.js.map +0 -1
- package/dist/CredentialServices-CgxwguAv.js.map +0 -1
- package/dist/PublicFrontendBootstrapFactory-Cb2pLmDd.d.ts +0 -45
- package/dist/PublicFrontendBootstrapJsonCodec-DzqvA0uo.js.map +0 -1
- package/dist/ServerLoggerFactory-BKSIh9Xv.js +0 -98
- package/dist/ServerLoggerFactory-BKSIh9Xv.js.map +0 -1
- package/dist/persistenceServer-vtJAGDat.d.ts +0 -9
- 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
|
|
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
|
|
24
|
+
private readonly entries = new Map<CredentialTypeId, RegistryEntry>();
|
|
25
|
+
private readonly bySource = new Map<CredentialTypeSource, Set<CredentialTypeId>>();
|
|
10
26
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
78
|
+
this.bySource.delete(source);
|
|
16
79
|
}
|
|
17
80
|
|
|
18
81
|
listTypes(): ReadonlyArray<CredentialTypeDefinition> {
|
|
19
|
-
return [...this.
|
|
82
|
+
return [...this.entries.values()].map((entry) => entry.type.definition);
|
|
20
83
|
}
|
|
21
84
|
|
|
22
85
|
getType(typeId: CredentialTypeId): CredentialTypeDefinition | undefined {
|
|
23
|
-
return this.
|
|
86
|
+
return this.entries.get(typeId)?.type.definition;
|
|
24
87
|
}
|
|
25
88
|
|
|
26
89
|
getCredentialType(typeId: CredentialTypeId): AnyCredentialType | undefined {
|
|
27
|
-
return this.
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
+
});
|