@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.
- package/CHANGELOG.md +431 -0
- package/LICENSE +1 -37
- package/dist/{ApiPaths-CLTHphYZ.js → ApiPaths-Dv1dcHu_.js} +4 -4
- package/dist/ApiPaths-Dv1dcHu_.js.map +1 -0
- package/dist/{AppConfigFactory-C6q-CSKb.js → AppConfigFactory-Cx4qQvRk.js} +112 -52
- package/dist/AppConfigFactory-Cx4qQvRk.js.map +1 -0
- package/dist/{AppConfigFactory-YnveXE9k.d.ts → AppConfigFactory-DnLoQ9Li.d.ts} +8490 -5548
- package/dist/{AppContainerFactory-qaqc-R1D.js → AppContainerFactory-DqKYCRNP.js} +7641 -2083
- package/dist/AppContainerFactory-DqKYCRNP.js.map +1 -0
- package/dist/{CodemationAppContext-DRu1Dpri.d.ts → CodemationAppContext-CKVv9W9q.d.ts} +8 -4
- package/dist/{CodemationAuthoring.types-fBRppnmi.d.ts → CodemationAuthoring.types-DA3G3s6d.d.ts} +25 -5
- package/dist/{CodemationAuthoring.types-DZl-sJaM.js → CodemationAuthoring.types-NGkBcmmT.js} +18 -6
- package/dist/CodemationAuthoring.types-NGkBcmmT.js.map +1 -0
- package/dist/{CodemationConfigNormalizer-DVko3cVN.d.ts → CodemationConfigNormalizer-BAKjetJ6.d.ts} +3 -3
- package/dist/{CodemationConsumerConfigLoader-BeAUS144.js → CodemationConsumerConfigLoader-GYpBBvqE.js} +79 -10
- package/dist/CodemationConsumerConfigLoader-GYpBBvqE.js.map +1 -0
- package/dist/{CodemationConsumerConfigLoader-DJWr86f-.d.ts → CodemationConsumerConfigLoader-nxOqvv46.d.ts} +17 -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-DKLAHT2b.d.ts} +123 -16
- 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-Be2I60Th.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-Ce1yxpnO.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-CY2FS-5g.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 +110 -21
- package/dist/index.js +15 -13
- package/dist/mapping.d.ts +2 -2
- package/dist/mapping.js +1 -1
- package/dist/nextServer.d.ts +43 -88
- 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-CmsIKnO9.js → persistenceServer-C-hH4z6l.js} +2 -2
- package/dist/{persistenceServer-CmsIKnO9.js.map → persistenceServer-C-hH4z6l.js.map} +1 -1
- package/dist/persistenceServer-CeTHtC6E.d.ts +30 -0
- package/dist/persistenceServer.d.ts +8 -8
- package/dist/persistenceServer.js +3 -3
- package/dist/{server-MUNGsBYK.d.ts → server-C4bS62rg.d.ts} +21 -6
- package/dist/{server-CJFfY67o.js → server-Y7kxwtCK.js} +7 -6
- package/dist/{server-CJFfY67o.js.map → server-Y7kxwtCK.js.map} +1 -1
- package/dist/server.d.ts +14 -14
- package/dist/server.js +13 -11
- package/package.json +29 -42
- 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 +277 -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/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 +6 -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 +54 -5
- package/src/presentation/config/CodemationConfig.ts +3 -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/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 +54 -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/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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import type {
|
|
3
|
+
OAuthFlowCallbackArgs,
|
|
4
|
+
OAuthFlowExecutor,
|
|
5
|
+
OAuthFlowStartArgs,
|
|
6
|
+
OAuthFlowStartResult,
|
|
7
|
+
OAuthMaterial,
|
|
8
|
+
} from "@codemation/core";
|
|
9
|
+
import type { LoggerFactory } from "../application/logging/Logger";
|
|
10
|
+
|
|
11
|
+
import { ApplicationTokens } from "../applicationTokens";
|
|
12
|
+
import { PairedFetch } from "../pairing/PairedFetch";
|
|
13
|
+
import { PairingConfigToken } from "../pairing/PairingConfigToken";
|
|
14
|
+
import type { PairingConfig } from "../pairing/pairing.types";
|
|
15
|
+
import { ManagedOAuthRefreshInvalidGrantError } from "./ManagedOAuthRefreshInvalidGrantError";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* OAuthFlowExecutor for managed mode (paired with a control plane).
|
|
19
|
+
*
|
|
20
|
+
* Delegates the entire OAuth dance to the control plane over HMAC-signed calls.
|
|
21
|
+
* Client secrets never leave the control plane.
|
|
22
|
+
*/
|
|
23
|
+
@injectable()
|
|
24
|
+
export class ManagedOAuthFlowExecutor implements OAuthFlowExecutor {
|
|
25
|
+
constructor(
|
|
26
|
+
@inject(PairedFetch) private readonly pairedFetch: PairedFetch,
|
|
27
|
+
@inject(PairingConfigToken) private readonly pairingConfig: PairingConfig,
|
|
28
|
+
@inject(ApplicationTokens.LoggerFactory) private readonly loggers: LoggerFactory,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
async start(args: OAuthFlowStartArgs): Promise<OAuthFlowStartResult> {
|
|
32
|
+
const logger = this.loggers.create("codemation.credentials.managed-oauth");
|
|
33
|
+
const url = `${this.pairingConfig.controlPlaneUrl}/internal/oauth/start`;
|
|
34
|
+
const response = await this.pairedFetch.post(url, {
|
|
35
|
+
typeId: args.typeId,
|
|
36
|
+
scopes: args.scopes,
|
|
37
|
+
redirectUri: args.redirectUri,
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const body = await response.text().catch(() => "");
|
|
41
|
+
const excerpt = body.slice(0, 200);
|
|
42
|
+
logger.warn(`ManagedOAuthFlowExecutor.start failed: ${response.status} ${excerpt}`);
|
|
43
|
+
throw new Error(`ManagedOAuthFlowExecutor.start failed: ${response.status} ${excerpt}`);
|
|
44
|
+
}
|
|
45
|
+
const json = (await response.json()) as { consentUrl: string; stateToken: string };
|
|
46
|
+
return { consentUrl: json.consentUrl, stateToken: json.stateToken };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lookupInstanceId(_stateToken: string): string | undefined {
|
|
50
|
+
// Managed mode — state is owned by the control plane, not the host.
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async completeCallback(args: OAuthFlowCallbackArgs): Promise<OAuthMaterial> {
|
|
55
|
+
const logger = this.loggers.create("codemation.credentials.managed-oauth");
|
|
56
|
+
const url = `${this.pairingConfig.controlPlaneUrl}/internal/oauth/complete`;
|
|
57
|
+
const response = await this.pairedFetch.post(url, {
|
|
58
|
+
stateToken: args.stateToken,
|
|
59
|
+
code: args.code,
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const body = await response.text().catch(() => "");
|
|
63
|
+
const excerpt = body.slice(0, 200);
|
|
64
|
+
logger.warn(`ManagedOAuthFlowExecutor.completeCallback failed: ${response.status} ${excerpt}`);
|
|
65
|
+
throw new Error(`ManagedOAuthFlowExecutor.completeCallback failed: ${response.status} ${excerpt}`);
|
|
66
|
+
}
|
|
67
|
+
return (await response.json()) as OAuthMaterial;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async refresh(args: { typeId: string; instanceId: string; material: OAuthMaterial }): Promise<OAuthMaterial> {
|
|
71
|
+
const { typeId, instanceId, material } = args;
|
|
72
|
+
if (!material.refreshToken) {
|
|
73
|
+
throw new Error("ManagedOAuthFlowExecutor.refresh: no refresh token available");
|
|
74
|
+
}
|
|
75
|
+
const url = `${this.pairingConfig.controlPlaneUrl}/internal/oauth/refresh`;
|
|
76
|
+
const response = await this.pairedFetch.post(url, {
|
|
77
|
+
typeId,
|
|
78
|
+
instanceId,
|
|
79
|
+
refreshToken: material.refreshToken,
|
|
80
|
+
});
|
|
81
|
+
if (response.status === 410) {
|
|
82
|
+
throw new ManagedOAuthRefreshInvalidGrantError(instanceId);
|
|
83
|
+
}
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`ManagedOAuthFlowExecutor.refresh failed: ${response.status}`);
|
|
86
|
+
}
|
|
87
|
+
const refreshed = (await response.json()) as OAuthMaterial;
|
|
88
|
+
// Preserve the existing refresh token if the control plane omits it from the response.
|
|
89
|
+
if (!refreshed.refreshToken) {
|
|
90
|
+
return { ...refreshed, refreshToken: material.refreshToken };
|
|
91
|
+
}
|
|
92
|
+
return refreshed;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when the control plane returns HTTP 410 (invalid_grant) during a refresh.
|
|
3
|
+
* The refresh token is dead — user revoked, token rotated away, etc.
|
|
4
|
+
* The credential cannot be auto-recovered; the user must reconnect via the Connect flow.
|
|
5
|
+
*/
|
|
6
|
+
export class ManagedOAuthRefreshInvalidGrantError extends Error {
|
|
7
|
+
constructor(readonly credentialInstanceId: string) {
|
|
8
|
+
super(
|
|
9
|
+
`Credential ${credentialInstanceId}: refresh token is invalid or revoked (invalid_grant). Reconnect required.`,
|
|
10
|
+
);
|
|
11
|
+
this.name = "ManagedOAuthRefreshInvalidGrantError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when the credential's refresh token is dead (user revoked the grant,
|
|
3
|
+
* or the token was rotated away). The installation cannot auto-recover; the user
|
|
4
|
+
* must reconnect via the broker Connect flow.
|
|
5
|
+
*/
|
|
6
|
+
export class CredentialDisconnectedError extends Error {
|
|
7
|
+
constructor(readonly credentialInstanceId: string) {
|
|
8
|
+
super(`Credential ${credentialInstanceId}: refresh token is invalid or revoked. Reconnect via the Connect flow.`);
|
|
9
|
+
this.name = "CredentialDisconnectedError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
WorkflowRepository,
|
|
8
8
|
} from "@codemation/core";
|
|
9
9
|
|
|
10
|
-
import { CoreTokens, inject, injectable } from "@codemation/core";
|
|
10
|
+
import { CoreTokens, CredentialUnboundError, inject, injectable } from "@codemation/core";
|
|
11
11
|
|
|
12
12
|
import { ApplicationRequestError } from "../../application/ApplicationRequestError";
|
|
13
13
|
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
} from "../../application/contracts/CredentialContractsRegistry";
|
|
18
18
|
|
|
19
19
|
import { ApplicationTokens } from "../../applicationTokens";
|
|
20
|
+
import type { Logger, LoggerFactory } from "../../application/logging/Logger";
|
|
20
21
|
|
|
21
22
|
import { WorkflowCredentialNodeResolver } from "./WorkflowCredentialNodeResolver";
|
|
22
23
|
import { CredentialInstanceService } from "./CredentialInstanceService";
|
|
@@ -24,6 +25,8 @@ import type { CredentialStore, MutableCredentialSessionService } from "./Credent
|
|
|
24
25
|
|
|
25
26
|
@injectable()
|
|
26
27
|
export class CredentialBindingService {
|
|
28
|
+
private readonly logger: Logger;
|
|
29
|
+
|
|
27
30
|
constructor(
|
|
28
31
|
@inject(ApplicationTokens.CredentialStore)
|
|
29
32
|
private readonly credentialStore: CredentialStore,
|
|
@@ -35,7 +38,11 @@ export class CredentialBindingService {
|
|
|
35
38
|
private readonly credentialSessionService: MutableCredentialSessionService,
|
|
36
39
|
@inject(WorkflowCredentialNodeResolver)
|
|
37
40
|
private readonly workflowCredentialNodeResolver: WorkflowCredentialNodeResolver,
|
|
38
|
-
|
|
41
|
+
@inject(ApplicationTokens.LoggerFactory)
|
|
42
|
+
loggerFactory: LoggerFactory,
|
|
43
|
+
) {
|
|
44
|
+
this.logger = loggerFactory.create("CredentialBindingService");
|
|
45
|
+
}
|
|
39
46
|
|
|
40
47
|
async upsertBinding(
|
|
41
48
|
args: Readonly<{ workflowId: string; nodeId: string; slotKey: string; instanceId: CredentialInstanceId }>,
|
|
@@ -63,6 +70,51 @@ export class CredentialBindingService {
|
|
|
63
70
|
return binding;
|
|
64
71
|
}
|
|
65
72
|
|
|
73
|
+
async assertRequiredCredentialsBound(workflowId: string): Promise<void> {
|
|
74
|
+
const workflow = this.requireWorkflow(workflowId);
|
|
75
|
+
const bindings = await this.credentialStore.listBindingsByWorkflowId(workflowId);
|
|
76
|
+
const boundKeys = new Set(bindings.map((b) => this.toBindingKeyString(b.key)));
|
|
77
|
+
const unboundByDb = this.workflowCredentialNodeResolver
|
|
78
|
+
.listSlots(workflow)
|
|
79
|
+
.filter((slot) => !slot.requirement.optional)
|
|
80
|
+
.filter(
|
|
81
|
+
(slot) =>
|
|
82
|
+
!boundKeys.has(
|
|
83
|
+
this.toBindingKeyString({ workflowId, nodeId: slot.nodeId, slotKey: slot.requirement.slotKey }),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
if (unboundByDb.length === 0) return;
|
|
87
|
+
// Confirm each apparently-unbound slot by attempting session resolution. A custom
|
|
88
|
+
// CredentialSessionService (e.g. a test harness) can satisfy slots that have no DB
|
|
89
|
+
// binding row; only slots that still fail are truly unresolvable.
|
|
90
|
+
const confirmed = [];
|
|
91
|
+
for (const slot of unboundByDb) {
|
|
92
|
+
try {
|
|
93
|
+
await this.credentialSessionService.getSession({
|
|
94
|
+
workflowId,
|
|
95
|
+
nodeId: slot.nodeId,
|
|
96
|
+
slotKey: slot.requirement.slotKey,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (!(error instanceof CredentialUnboundError)) {
|
|
100
|
+
this.logger.debug(
|
|
101
|
+
`CredentialBindingService: unexpected error resolving session for slot ${slot.requirement.slotKey} on ${slot.nodeId}`,
|
|
102
|
+
error instanceof Error ? error : undefined,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
confirmed.push(slot);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (confirmed.length === 0) return;
|
|
109
|
+
const descriptions = confirmed
|
|
110
|
+
.map((slot) => `"${slot.requirement.label}" on ${slot.nodeName ?? slot.nodeId}`)
|
|
111
|
+
.join(", ");
|
|
112
|
+
throw new ApplicationRequestError(
|
|
113
|
+
400,
|
|
114
|
+
`Cannot run workflow: required credential slot${confirmed.length > 1 ? "s" : ""} not bound: ${descriptions}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
66
118
|
async listWorkflowHealth(workflowId: string): Promise<WorkflowCredentialHealthDto> {
|
|
67
119
|
const workflow = this.requireWorkflow(workflowId);
|
|
68
120
|
const bindings = await this.credentialStore.listBindingsByWorkflowId(workflowId);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by {@link CredentialSecretCipher.decrypt} when the credential's stored
|
|
3
|
+
* `encryptionKeyId` does not match the current master key's id.
|
|
4
|
+
*
|
|
5
|
+
* This indicates the `CODEMATION_CREDENTIALS_MASTER_KEY` environment variable has
|
|
6
|
+
* been rotated since the credential was encrypted. The operator must re-bind the
|
|
7
|
+
* affected credential (which re-encrypts it with the new key).
|
|
8
|
+
*
|
|
9
|
+
* See {@link docs/security-boundary.md} for the key rotation contract.
|
|
10
|
+
*/
|
|
11
|
+
export class CredentialKeyRotatedError extends Error {
|
|
12
|
+
readonly storedKeyId: string;
|
|
13
|
+
|
|
14
|
+
constructor(storedKeyId: string) {
|
|
15
|
+
super(
|
|
16
|
+
`Credential was encrypted with key "${storedKeyId}" but the current master key produces a different id. ` +
|
|
17
|
+
`Re-bind the credential to re-encrypt it with the active key.`,
|
|
18
|
+
);
|
|
19
|
+
this.name = "CredentialKeyRotatedError";
|
|
20
|
+
this.storedKeyId = storedKeyId;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { inject, injectable } from "@codemation/core";
|
|
4
4
|
|
|
@@ -6,13 +6,26 @@ import { ApplicationTokens } from "../../applicationTokens";
|
|
|
6
6
|
import type { AppConfig } from "../../presentation/config/AppConfig";
|
|
7
7
|
|
|
8
8
|
import type { JsonRecord } from "./CredentialServices";
|
|
9
|
+
import { CredentialKeyRotatedError } from "./CredentialKeyRotatedError";
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Schema versions:
|
|
13
|
+
* 1 — key = SHA-256(rawValue) (legacy, read-only support retained for migration)
|
|
14
|
+
* 2 — key = HKDF-SHA-256(rawKey32Bytes, ...) (current)
|
|
15
|
+
*
|
|
16
|
+
* All new encryptions are written as v2. Existing v1 records can still be
|
|
17
|
+
* decrypted so operators can re-encrypt at their own pace (re-bind the
|
|
18
|
+
* credential in the UI, or run the one-shot re-encrypt script).
|
|
19
|
+
*/
|
|
10
20
|
@injectable()
|
|
11
21
|
export class CredentialSecretCipher {
|
|
12
22
|
private static readonly algorithm = "aes-256-gcm";
|
|
13
|
-
private static readonly
|
|
23
|
+
private static readonly currentSchemaVersion = 2;
|
|
14
24
|
private static readonly ivLength = 12;
|
|
15
25
|
|
|
26
|
+
private static readonly HKDF_SALT = "codemation/credential-cipher/v1";
|
|
27
|
+
private static readonly HKDF_INFO = "aes-256-gcm-key";
|
|
28
|
+
|
|
16
29
|
constructor(
|
|
17
30
|
@inject(ApplicationTokens.AppConfig)
|
|
18
31
|
private readonly appConfig: AppConfig,
|
|
@@ -24,7 +37,7 @@ export class CredentialSecretCipher {
|
|
|
24
37
|
schemaVersion: number;
|
|
25
38
|
}> {
|
|
26
39
|
const iv = randomBytes(CredentialSecretCipher.ivLength);
|
|
27
|
-
const cipher = createCipheriv(CredentialSecretCipher.algorithm, this.
|
|
40
|
+
const cipher = createCipheriv(CredentialSecretCipher.algorithm, this.resolveKeyMaterialV2(), iv);
|
|
28
41
|
const plaintext = Buffer.from(JSON.stringify(value), "utf8");
|
|
29
42
|
// eslint-disable-next-line codemation/no-buffer-everything -- AES-GCM credential cipher operates on bounded KB-sized JSON payloads; streaming crypto is not applicable here.
|
|
30
43
|
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
@@ -33,7 +46,7 @@ export class CredentialSecretCipher {
|
|
|
33
46
|
// eslint-disable-next-line codemation/no-buffer-everything -- AES-GCM credential cipher operates on bounded KB-sized JSON payloads; streaming crypto is not applicable here.
|
|
34
47
|
encryptedJson: Buffer.concat([iv, authTag, encrypted]).toString("base64"),
|
|
35
48
|
encryptionKeyId: this.resolveKeyId(),
|
|
36
|
-
schemaVersion: CredentialSecretCipher.
|
|
49
|
+
schemaVersion: CredentialSecretCipher.currentSchemaVersion,
|
|
37
50
|
};
|
|
38
51
|
}
|
|
39
52
|
|
|
@@ -44,19 +57,48 @@ export class CredentialSecretCipher {
|
|
|
44
57
|
schemaVersion: number;
|
|
45
58
|
}>,
|
|
46
59
|
): JsonRecord {
|
|
60
|
+
// resolveKeyMaterialV2 / resolveKeyMaterialV1 both throw if env is missing
|
|
61
|
+
// — that check must come before the key-id comparison.
|
|
62
|
+
const keyMaterial = (record.schemaVersion ?? 1) >= 2 ? this.resolveKeyMaterialV2() : this.resolveKeyMaterialV1();
|
|
63
|
+
|
|
64
|
+
const currentKeyId = this.resolveKeyId();
|
|
65
|
+
if (record.encryptionKeyId !== currentKeyId) {
|
|
66
|
+
throw new CredentialKeyRotatedError(record.encryptionKeyId);
|
|
67
|
+
}
|
|
47
68
|
// eslint-disable-next-line codemation/no-buffer-everything -- AES-GCM credential cipher operates on bounded KB-sized JSON payloads; streaming crypto is not applicable here.
|
|
48
69
|
const packed = Buffer.from(record.encryptedJson, "base64");
|
|
49
70
|
const iv = packed.subarray(0, CredentialSecretCipher.ivLength);
|
|
50
71
|
const authTag = packed.subarray(CredentialSecretCipher.ivLength, CredentialSecretCipher.ivLength + 16);
|
|
51
72
|
const encrypted = packed.subarray(CredentialSecretCipher.ivLength + 16);
|
|
52
|
-
const decipher = createDecipheriv(CredentialSecretCipher.algorithm,
|
|
73
|
+
const decipher = createDecipheriv(CredentialSecretCipher.algorithm, keyMaterial, iv);
|
|
53
74
|
decipher.setAuthTag(authTag);
|
|
54
75
|
// eslint-disable-next-line codemation/no-buffer-everything -- AES-GCM credential cipher operates on bounded KB-sized JSON payloads; streaming crypto is not applicable here.
|
|
55
76
|
const plaintext = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
|
|
56
77
|
return JSON.parse(plaintext) as JsonRecord;
|
|
57
78
|
}
|
|
58
79
|
|
|
59
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Current (v2) key derivation: HKDF-SHA-256 with a fixed application salt and info label.
|
|
82
|
+
* Input must be a base64-encoded 32-byte value (`CODEMATION_CREDENTIALS_MASTER_KEY`).
|
|
83
|
+
*/
|
|
84
|
+
private resolveKeyMaterialV2(): Buffer {
|
|
85
|
+
const ikm = this.resolveBase64Key32Bytes();
|
|
86
|
+
return Buffer.from(
|
|
87
|
+
hkdfSync(
|
|
88
|
+
"sha256",
|
|
89
|
+
ikm,
|
|
90
|
+
Buffer.from(CredentialSecretCipher.HKDF_SALT, "utf8"),
|
|
91
|
+
Buffer.from(CredentialSecretCipher.HKDF_INFO, "utf8"),
|
|
92
|
+
32,
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Legacy (v1) key derivation: SHA-256 of the raw env string.
|
|
99
|
+
* Retained for decrypt-side backward compatibility only.
|
|
100
|
+
*/
|
|
101
|
+
private resolveKeyMaterialV1(): Buffer {
|
|
60
102
|
const rawValue = this.appConfig.env.CODEMATION_CREDENTIALS_MASTER_KEY;
|
|
61
103
|
if (!rawValue || rawValue.trim().length === 0) {
|
|
62
104
|
throw new Error("CODEMATION_CREDENTIALS_MASTER_KEY is required to encrypt database-managed credentials.");
|
|
@@ -64,6 +106,26 @@ export class CredentialSecretCipher {
|
|
|
64
106
|
return createHash("sha256").update(rawValue).digest();
|
|
65
107
|
}
|
|
66
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Validates and returns the raw 32-byte key material from the env var.
|
|
111
|
+
* Throws if the env var is absent or does not decode to exactly 32 bytes.
|
|
112
|
+
*/
|
|
113
|
+
private resolveBase64Key32Bytes(): Buffer {
|
|
114
|
+
const rawValue = this.appConfig.env.CODEMATION_CREDENTIALS_MASTER_KEY;
|
|
115
|
+
if (!rawValue || rawValue.trim().length === 0) {
|
|
116
|
+
throw new Error("CODEMATION_CREDENTIALS_MASTER_KEY is required to encrypt database-managed credentials.");
|
|
117
|
+
}
|
|
118
|
+
// eslint-disable-next-line codemation/no-buffer-everything -- key material is always 32 bytes; bounded by validation below.
|
|
119
|
+
const decoded = Buffer.from(rawValue.trim(), "base64");
|
|
120
|
+
if (decoded.length !== 32) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`CODEMATION_CREDENTIALS_MASTER_KEY must be a base64-encoded 32-byte value (got ${decoded.length} bytes). ` +
|
|
123
|
+
`Generate a valid key with: openssl rand -base64 32`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return decoded;
|
|
127
|
+
}
|
|
128
|
+
|
|
67
129
|
private resolveKeyId(): string {
|
|
68
130
|
const rawValue = this.appConfig.env.CODEMATION_CREDENTIALS_MASTER_KEY;
|
|
69
131
|
return createHash("sha256")
|
|
@@ -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 {
|