@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
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
} from "@codemation/core";
|
|
9
9
|
import type { CodemationContainerRegistration } from "../../bootstrap/CodemationContainerRegistration";
|
|
10
10
|
import type { CodemationAppContext } from "./CodemationAppContext";
|
|
11
|
+
import type { CodemationAuthConfig } from "./CodemationAuthConfig";
|
|
11
12
|
import type { CodemationClassToken } from "./CodemationClassToken";
|
|
12
13
|
import type {
|
|
13
14
|
CodemationApplicationRuntimeConfig,
|
|
@@ -25,6 +26,9 @@ export type NormalizedCodemationConfig = Omit<CodemationConfig, "collections"> &
|
|
|
25
26
|
|
|
26
27
|
export class CodemationConfigNormalizer {
|
|
27
28
|
normalize(config: CodemationConfig): NormalizedCodemationConfig {
|
|
29
|
+
const auth = config.app?.auth ?? config.auth;
|
|
30
|
+
this.assertAuthConfig(auth);
|
|
31
|
+
this.assertManagedModeConstraints(config, auth);
|
|
28
32
|
const collected = this.collectRegistration(config);
|
|
29
33
|
const normalizedRuntime = this.normalizeRuntimeConfig(config);
|
|
30
34
|
const normalizedWorkflowDiscoveryDirectories = [
|
|
@@ -34,7 +38,7 @@ export class CodemationConfigNormalizer {
|
|
|
34
38
|
|
|
35
39
|
return {
|
|
36
40
|
...config,
|
|
37
|
-
auth
|
|
41
|
+
auth,
|
|
38
42
|
containerRegistrations: collected.containerRegistrations,
|
|
39
43
|
credentialTypes: [...(config.credentialTypes ?? []), ...collected.credentialTypes],
|
|
40
44
|
collections: [...this.unwrapCollections(config.collections), ...collected.collections],
|
|
@@ -49,6 +53,23 @@ export class CodemationConfigNormalizer {
|
|
|
49
53
|
};
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Enforces managed-mode invariants beyond what `assertAuthConfig` covers:
|
|
58
|
+
* managed-mode workspaces are always Postgres and always require at least one workflow source.
|
|
59
|
+
*/
|
|
60
|
+
private assertManagedModeConstraints(config: CodemationConfig, auth: CodemationAuthConfig | undefined): void {
|
|
61
|
+
if (auth?.kind !== "managed") {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const hasWorkflows = (config.workflows?.length ?? 0) > 0;
|
|
65
|
+
const hasWorkflowDiscovery = (config.workflowDiscovery?.directories?.length ?? 0) > 0;
|
|
66
|
+
if (!hasWorkflows && !hasWorkflowDiscovery) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Managed-mode workspaces require at least one workflow source. Provide "workflows" or "workflowsDir" (which maps to workflowDiscovery.directories) in defineCodemationApp.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
private collectRegistration(config: CodemationConfig): Readonly<{
|
|
53
74
|
containerRegistrations: ReadonlyArray<CodemationContainerRegistration<unknown>>;
|
|
54
75
|
credentialTypes: ReadonlyArray<AnyCredentialType>;
|
|
@@ -187,6 +208,23 @@ export class CodemationConfigNormalizer {
|
|
|
187
208
|
};
|
|
188
209
|
}
|
|
189
210
|
|
|
211
|
+
private assertAuthConfig(authConfig: CodemationConfig["auth"]): void {
|
|
212
|
+
if (authConfig?.kind !== "managed") {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (authConfig.oauth && authConfig.oauth.length > 0) {
|
|
216
|
+
throw new Error('auth.kind "managed" cannot be combined with oauth providers. Remove the oauth config.');
|
|
217
|
+
}
|
|
218
|
+
if (authConfig.oidc && authConfig.oidc.length > 0) {
|
|
219
|
+
throw new Error('auth.kind "managed" cannot be combined with oidc providers. Remove the oidc config.');
|
|
220
|
+
}
|
|
221
|
+
if (authConfig.allowUnauthenticatedInDevelopment === true) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
'auth.kind "managed" cannot be combined with allowUnauthenticatedInDevelopment. Remove that flag.',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
190
228
|
private mergeWorkflows(
|
|
191
229
|
configuredWorkflows: ReadonlyArray<WorkflowDefinition>,
|
|
192
230
|
registeredWorkflows: ReadonlyArray<WorkflowDefinition>,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyCredentialType, Container } from "@codemation/core";
|
|
1
|
+
import type { AnyCredentialType, Container, McpServerDeclaration } from "@codemation/core";
|
|
2
2
|
import type { LoggerFactory } from "../../application/logging/Logger";
|
|
3
3
|
import type { AppConfig } from "./AppConfig";
|
|
4
4
|
import type { CodemationRegistrationContextBase } from "./CodemationAppContext";
|
|
@@ -13,6 +13,7 @@ export interface CodemationPluginContext extends CodemationRegistrationContextBa
|
|
|
13
13
|
export interface CodemationPluginConfig {
|
|
14
14
|
readonly pluginPackageId?: string;
|
|
15
15
|
readonly credentialTypes?: ReadonlyArray<AnyCredentialType>;
|
|
16
|
+
readonly mcpServers?: ReadonlyArray<McpServerDeclaration>;
|
|
16
17
|
readonly register?: (context: CodemationPluginContext) => void | Promise<void>;
|
|
17
18
|
readonly sandbox?: CodemationConfig;
|
|
18
19
|
}
|
|
@@ -11,4 +11,9 @@ export type CodemationFrontendAuthSnapshot = Readonly<{
|
|
|
11
11
|
oauthProviders: ReadonlyArray<CodemationFrontendAuthProviderSnapshot>;
|
|
12
12
|
secret: string | null;
|
|
13
13
|
uiAuthEnabled: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Present when auth.kind === "managed". Carries the CP-web origin for CORS
|
|
16
|
+
* awareness. No UI login flow is rendered in managed mode.
|
|
17
|
+
*/
|
|
18
|
+
cpWebOrigin?: string;
|
|
14
19
|
}>;
|
|
@@ -29,13 +29,19 @@ export class CodemationFrontendAuthSnapshotFactory {
|
|
|
29
29
|
uiAuthEnabled: boolean;
|
|
30
30
|
}>,
|
|
31
31
|
): CodemationFrontendAuthSnapshot {
|
|
32
|
-
|
|
32
|
+
const snapshot = {
|
|
33
33
|
config: args.authConfig,
|
|
34
34
|
credentialsEnabled: args.authConfig?.kind === "local",
|
|
35
35
|
oauthProviders: this.createOauthProviders(args.authConfig),
|
|
36
36
|
secret: this.resolveAuthSecret(args.env),
|
|
37
37
|
uiAuthEnabled: args.uiAuthEnabled,
|
|
38
38
|
};
|
|
39
|
+
const cpWebOrigin =
|
|
40
|
+
args.authConfig?.kind === "managed" ? args.env["CP_WEB_ORIGIN"]?.trim() || undefined : undefined;
|
|
41
|
+
if (cpWebOrigin) {
|
|
42
|
+
return { ...snapshot, cpWebOrigin };
|
|
43
|
+
}
|
|
44
|
+
return snapshot;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
private resolveUiAuthEnabled(authConfig: CodemationAuthConfig | undefined, env: NodeJS.ProcessEnv): boolean {
|
|
@@ -9,4 +9,6 @@ export type PublicFrontendBootstrap = Readonly<{
|
|
|
9
9
|
oauthProviders: ReadonlyArray<CodemationFrontendAuthProviderSnapshot>;
|
|
10
10
|
productName: string;
|
|
11
11
|
uiAuthEnabled: boolean;
|
|
12
|
+
/** Present in managed mode — the CP web origin. No UI login is rendered when this is set. */
|
|
13
|
+
cpWebOrigin?: string;
|
|
12
14
|
}>;
|
|
@@ -12,12 +12,16 @@ export class PublicFrontendBootstrapFactory {
|
|
|
12
12
|
|
|
13
13
|
create(): PublicFrontendBootstrap {
|
|
14
14
|
const frontendAppConfig = this.frontendAppConfigFactory.create();
|
|
15
|
-
|
|
15
|
+
const bootstrap: PublicFrontendBootstrap = {
|
|
16
16
|
credentialsEnabled: frontendAppConfig.auth.credentialsEnabled,
|
|
17
17
|
logoUrl: frontendAppConfig.logoUrl,
|
|
18
18
|
oauthProviders: frontendAppConfig.auth.oauthProviders,
|
|
19
19
|
productName: frontendAppConfig.productName,
|
|
20
20
|
uiAuthEnabled: frontendAppConfig.auth.uiAuthEnabled,
|
|
21
21
|
};
|
|
22
|
+
if (frontendAppConfig.auth.cpWebOrigin) {
|
|
23
|
+
return { ...bootstrap, cpWebOrigin: frontendAppConfig.auth.cpWebOrigin };
|
|
24
|
+
}
|
|
25
|
+
return bootstrap;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
@@ -15,7 +15,7 @@ export class PublicFrontendBootstrapJsonCodec {
|
|
|
15
15
|
if (!parsed || typeof parsed !== "object") {
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
const base: PublicFrontendBootstrap = {
|
|
19
19
|
credentialsEnabled: parsed.credentialsEnabled === true,
|
|
20
20
|
logoUrl: typeof parsed.logoUrl === "string" && parsed.logoUrl.trim().length > 0 ? parsed.logoUrl : null,
|
|
21
21
|
oauthProviders: this.resolveOauthProviders(parsed.oauthProviders),
|
|
@@ -25,6 +25,9 @@ export class PublicFrontendBootstrapJsonCodec {
|
|
|
25
25
|
: "Codemation",
|
|
26
26
|
uiAuthEnabled: parsed.uiAuthEnabled !== false,
|
|
27
27
|
};
|
|
28
|
+
const cpWebOrigin =
|
|
29
|
+
typeof parsed.cpWebOrigin === "string" && parsed.cpWebOrigin.trim().length > 0 ? parsed.cpWebOrigin : undefined;
|
|
30
|
+
return cpWebOrigin ? { ...base, cpWebOrigin } : base;
|
|
28
31
|
} catch {
|
|
29
32
|
return null;
|
|
30
33
|
}
|
|
@@ -145,10 +145,6 @@ export class ApiPaths {
|
|
|
145
145
|
return `${this.apiBasePath}/credential-bindings`;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
static oauth2Auth(instanceId: string): string {
|
|
149
|
-
return `${this.oauth2BasePath}/auth?instanceId=${encodeURIComponent(instanceId)}`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
148
|
static oauth2RedirectUri(): string {
|
|
153
149
|
return `${this.oauth2BasePath}/redirect-uri`;
|
|
154
150
|
}
|
|
@@ -157,6 +153,10 @@ export class ApiPaths {
|
|
|
157
153
|
return `${this.oauth2BasePath}/disconnect?instanceId=${encodeURIComponent(instanceId)}`;
|
|
158
154
|
}
|
|
159
155
|
|
|
156
|
+
static credentialOAuthStart(): string {
|
|
157
|
+
return `${this.credentialsBasePath}/oauth/start`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
160
|
static workflowWebsocket(): string {
|
|
161
161
|
return `${this.workflowsBasePath}/ws`;
|
|
162
162
|
}
|
|
@@ -1,13 +1,50 @@
|
|
|
1
1
|
import { ApplicationRequestError } from "../../application/ApplicationRequestError";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Shape of the JSON body returned on an unhandled 500. The canvas (and any other client)
|
|
5
|
+
* reads `message` + optional `stack` + optional `cause` to surface a copy/pastable error
|
|
6
|
+
* dialog. Generic "Internal server error" with no detail makes operator triage impossible
|
|
7
|
+
* — this contract preserves the diagnostic information the CLI logs anyway.
|
|
8
|
+
*/
|
|
9
|
+
export type ServerHttpUnhandledErrorPayload = Readonly<{
|
|
10
|
+
error: "Internal server error";
|
|
11
|
+
message: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
stack?: string;
|
|
14
|
+
cause?: string;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
3
17
|
export class ServerHttpErrorResponseFactory {
|
|
4
18
|
static fromUnknown(error: unknown): Response {
|
|
5
19
|
if (error instanceof ApplicationRequestError) {
|
|
6
20
|
return Response.json(error.payload, { status: error.status });
|
|
7
21
|
}
|
|
8
22
|
this.logUnexpectedError(error);
|
|
9
|
-
|
|
10
|
-
|
|
23
|
+
return Response.json(this.toUnhandledPayload(error), { status: 500 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private static toUnhandledPayload(error: unknown): ServerHttpUnhandledErrorPayload {
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
return {
|
|
29
|
+
error: "Internal server error",
|
|
30
|
+
message: error.message || `${error.name}: <no message>`,
|
|
31
|
+
name: error.name,
|
|
32
|
+
stack: error.stack,
|
|
33
|
+
cause: this.formatCauseValue(error),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return { error: "Internal server error", message: String(error) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private static formatCauseValue(error: Error): string | undefined {
|
|
40
|
+
if (!("cause" in error) || !error.cause) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const cause = error.cause;
|
|
44
|
+
if (cause instanceof Error) {
|
|
45
|
+
return cause.stack ?? `${cause.name}: ${cause.message}`;
|
|
46
|
+
}
|
|
47
|
+
return String(cause);
|
|
11
48
|
}
|
|
12
49
|
|
|
13
50
|
private static logUnexpectedError(error: unknown): void {
|
|
@@ -5,7 +5,9 @@ import { ApplicationTokens } from "../../../applicationTokens";
|
|
|
5
5
|
import { BinaryHttpRouteHandler } from "../routeHandlers/BinaryHttpRouteHandlerFactory";
|
|
6
6
|
import { ServerHttpErrorResponseFactory } from "../ServerHttpErrorResponseFactory";
|
|
7
7
|
import type { HonoApiRouteRegistrar } from "./HonoApiRouteRegistrar";
|
|
8
|
+
import type { InternalHonoApiRouteRegistrar } from "./InternalHonoApiRouteRegistrar";
|
|
8
9
|
import { HonoHttpAnonymousRoutePolicy } from "./HonoHttpAnonymousRoutePolicyRegistry";
|
|
10
|
+
import { ManagedCorsMiddleware } from "../../../auth/managed/ManagedCorsMiddleware";
|
|
9
11
|
|
|
10
12
|
@injectable()
|
|
11
13
|
export class CodemationHonoApiApp {
|
|
@@ -18,10 +20,21 @@ export class CodemationHonoApiApp {
|
|
|
18
20
|
registrars: ReadonlyArray<HonoApiRouteRegistrar>,
|
|
19
21
|
@inject(BinaryHttpRouteHandler)
|
|
20
22
|
binaryHttpRouteHandler: BinaryHttpRouteHandler,
|
|
23
|
+
@injectAll(ApplicationTokens.InternalHonoApiRouteRegistrar, { isOptional: true })
|
|
24
|
+
internalRegistrars: ReadonlyArray<InternalHonoApiRouteRegistrar>,
|
|
25
|
+
@injectAll(ApplicationTokens.ManagedCorsMiddleware, { isOptional: true })
|
|
26
|
+
corsMiddlewareList: ReadonlyArray<ManagedCorsMiddleware>,
|
|
21
27
|
) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
// Root app — composes /api/* (auth-gated) and /internal/* (HMAC-gated) sub-apps.
|
|
29
|
+
const root = new Hono();
|
|
30
|
+
const corsMiddleware = corsMiddlewareList[0] ?? null;
|
|
31
|
+
if (corsMiddleware) {
|
|
32
|
+
root.use("*", corsMiddleware.handle());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const api = new Hono().basePath("/api");
|
|
36
|
+
api.onError((error, _c) => ServerHttpErrorResponseFactory.fromUnknown(error));
|
|
37
|
+
api.use("*", async (c, next) => {
|
|
25
38
|
if (HonoHttpAnonymousRoutePolicy.isAnonymousRoute(c.req.raw)) {
|
|
26
39
|
await next();
|
|
27
40
|
return;
|
|
@@ -33,25 +46,37 @@ export class CodemationHonoApiApp {
|
|
|
33
46
|
await next();
|
|
34
47
|
});
|
|
35
48
|
for (const registrar of registrars) {
|
|
36
|
-
registrar.register(
|
|
49
|
+
registrar.register(api);
|
|
37
50
|
}
|
|
38
|
-
|
|
51
|
+
api.get("/workflows/:workflowId/debugger-overlay/binary/:binaryId/content", (c) =>
|
|
39
52
|
binaryHttpRouteHandler.getWorkflowOverlayBinaryContent(c.req.raw, {
|
|
40
53
|
workflowId: c.req.param("workflowId"),
|
|
41
54
|
binaryId: c.req.param("binaryId"),
|
|
42
55
|
}),
|
|
43
56
|
);
|
|
44
|
-
|
|
57
|
+
api.post("/workflows/:workflowId/debugger-overlay/binary/upload", (c) =>
|
|
45
58
|
binaryHttpRouteHandler.postWorkflowDebuggerOverlayBinaryUpload(c.req.raw, {
|
|
46
59
|
workflowId: c.req.param("workflowId"),
|
|
47
60
|
}),
|
|
48
61
|
);
|
|
49
|
-
|
|
62
|
+
api.notFound((c) => {
|
|
50
63
|
const method = c.req.method.toUpperCase();
|
|
51
64
|
const url = new URL(c.req.url);
|
|
52
65
|
return c.json({ error: `Unknown API route: ${method} ${url.pathname}` }, 404);
|
|
53
66
|
});
|
|
54
|
-
|
|
67
|
+
|
|
68
|
+
root.route("/", api);
|
|
69
|
+
|
|
70
|
+
// /internal/* routes — only mounted when pairing is configured.
|
|
71
|
+
if (internalRegistrars.length > 0) {
|
|
72
|
+
const internal = new Hono();
|
|
73
|
+
for (const registrar of internalRegistrars) {
|
|
74
|
+
registrar.register(internal);
|
|
75
|
+
}
|
|
76
|
+
root.route("/", internal);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.app = root;
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
getHono(): Hono {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Registrar interface for routes mounted on the installation's internal Hono app
|
|
5
|
+
* (no `/api` prefix). All routes registered here are accessible at `/internal/<path>`
|
|
6
|
+
* and are protected by HMAC auth middleware.
|
|
7
|
+
*
|
|
8
|
+
* See docs/pairing-protocol.md for the wire format and auth requirements.
|
|
9
|
+
*/
|
|
10
|
+
export interface InternalHonoApiRouteRegistrar {
|
|
11
|
+
register(app: Hono): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import type { HonoApiRouteRegistrar } from "../HonoApiRouteRegistrar";
|
|
4
|
+
import type { SessionVerifier } from "../../../../application/auth/SessionVerifier";
|
|
5
|
+
import { ApplicationTokens } from "../../../../applicationTokens";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Exposes `GET /api/me` in managed-auth mode.
|
|
9
|
+
*
|
|
10
|
+
* Reads the JWT principal by re-verifying the Bearer token, and returns
|
|
11
|
+
* `{ userId, workspaceId }`. No DB lookup needed — the JWT is the source of truth.
|
|
12
|
+
*
|
|
13
|
+
* Only registered when `auth.kind === "managed"`.
|
|
14
|
+
*/
|
|
15
|
+
@injectable()
|
|
16
|
+
export class ManagedMeHonoApiRouteRegistrar implements HonoApiRouteRegistrar {
|
|
17
|
+
constructor(
|
|
18
|
+
@inject(ApplicationTokens.SessionVerifier)
|
|
19
|
+
private readonly sessionVerifier: SessionVerifier,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
register(app: Hono): void {
|
|
23
|
+
app.get("/me", async (c) => {
|
|
24
|
+
try {
|
|
25
|
+
const principal = await this.sessionVerifier.verify(c.req.raw);
|
|
26
|
+
if (!principal) {
|
|
27
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
28
|
+
}
|
|
29
|
+
return c.json({ userId: principal.id, workspaceId: principal.workspaceId ?? null });
|
|
30
|
+
} catch {
|
|
31
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -8,8 +8,8 @@ export class OAuth2HonoApiRouteRegistrar implements HonoApiRouteRegistrar {
|
|
|
8
8
|
constructor(@inject(OAuth2HttpRouteHandler) private readonly handler: OAuth2HttpRouteHandler) {}
|
|
9
9
|
|
|
10
10
|
register(app: Hono): void {
|
|
11
|
-
app.
|
|
12
|
-
app.get("/oauth2/callback", (c) => this.handler.
|
|
11
|
+
app.post("/credentials/oauth/start", (c) => this.handler.postOAuthStart(c.req.raw));
|
|
12
|
+
app.get("/oauth2/callback", (c) => this.handler.getOAuthCallback(c.req.raw));
|
|
13
13
|
app.get("/oauth2/redirect-uri", (c) => this.handler.getRedirectUri(c.req.raw));
|
|
14
14
|
app.post("/oauth2/disconnect", (c) => this.handler.postDisconnect(c.req.raw));
|
|
15
15
|
}
|
|
@@ -2,6 +2,7 @@ import { inject, injectable } from "@codemation/core";
|
|
|
2
2
|
import { HttpRequestJsonBodyReader } from "../HttpRequestJsonBodyReader";
|
|
3
3
|
import type { CommandBus } from "../../../application/bus/CommandBus";
|
|
4
4
|
import type { QueryBus } from "../../../application/bus/QueryBus";
|
|
5
|
+
import type { SessionVerifier } from "../../../application/auth/SessionVerifier";
|
|
5
6
|
import {
|
|
6
7
|
CreateCredentialInstanceCommand,
|
|
7
8
|
DeleteCredentialInstanceCommand,
|
|
@@ -23,6 +24,8 @@ import {
|
|
|
23
24
|
ListCredentialTypesQuery,
|
|
24
25
|
} from "../../../application/queries/CredentialQueryHandlers";
|
|
25
26
|
import { ApplicationTokens } from "../../../applicationTokens";
|
|
27
|
+
import type { PairingConfig } from "../../../pairing/pairing.types";
|
|
28
|
+
import { PairingConfigToken } from "../../../pairing/PairingConfigToken";
|
|
26
29
|
import { ServerHttpErrorResponseFactory } from "../ServerHttpErrorResponseFactory";
|
|
27
30
|
import type { ServerHttpRouteParams } from "../ServerHttpRouteParams";
|
|
28
31
|
|
|
@@ -33,6 +36,10 @@ export class CredentialHttpRouteHandler {
|
|
|
33
36
|
private readonly queryBus: QueryBus,
|
|
34
37
|
@inject(ApplicationTokens.CommandBus)
|
|
35
38
|
private readonly commandBus: CommandBus,
|
|
39
|
+
@inject(ApplicationTokens.SessionVerifier)
|
|
40
|
+
private readonly sessionVerifier: SessionVerifier,
|
|
41
|
+
@inject(PairingConfigToken, { isOptional: true })
|
|
42
|
+
private readonly pairingConfig: PairingConfig | null,
|
|
36
43
|
) {}
|
|
37
44
|
|
|
38
45
|
async getCredentialTypes(): Promise<Response> {
|
|
@@ -62,6 +69,27 @@ export class CredentialHttpRouteHandler {
|
|
|
62
69
|
async getCredentialInstance(request: Request, params: ServerHttpRouteParams): Promise<Response> {
|
|
63
70
|
try {
|
|
64
71
|
const withSecrets = new URL(request.url).searchParams.get("withSecrets") === "1";
|
|
72
|
+
|
|
73
|
+
if (withSecrets) {
|
|
74
|
+
// Ownership check: fail-closed.
|
|
75
|
+
// - If the session verifier returns null (unauthenticated), reject.
|
|
76
|
+
// - In managed-JWT mode the principal's workspaceId must match the
|
|
77
|
+
// installation's own workspaceId (from PairingConfig).
|
|
78
|
+
// - In local-auth mode (pairingConfig absent) a valid non-null principal
|
|
79
|
+
// is sufficient — no cross-workspace check is possible or needed.
|
|
80
|
+
const principal = await this.sessionVerifier.verify(request);
|
|
81
|
+
if (!principal) {
|
|
82
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
principal.source === "managed-jwt" &&
|
|
86
|
+
this.pairingConfig !== null &&
|
|
87
|
+
principal.workspaceId !== this.pairingConfig.workspaceId
|
|
88
|
+
) {
|
|
89
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
65
93
|
const instance = withSecrets
|
|
66
94
|
? await this.queryBus.execute(new GetCredentialInstanceWithSecretsQuery(params.instanceId!))
|
|
67
95
|
: await this.queryBus.execute(new GetCredentialInstanceQuery(params.instanceId!));
|
|
@@ -1,79 +1,136 @@
|
|
|
1
|
+
import type { OAuthFlowExecutor } from "@codemation/core";
|
|
1
2
|
import { inject, injectable } from "@codemation/core";
|
|
2
3
|
import serialize from "serialize-javascript";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { ApplicationTokens } from "../../../applicationTokens";
|
|
5
|
+
import {
|
|
6
|
+
CredentialInstanceService,
|
|
7
|
+
CredentialSecretCipher,
|
|
8
|
+
type CredentialStore,
|
|
9
|
+
} from "../../../domain/credentials/CredentialServices";
|
|
10
|
+
import { OAuth2RedirectUriResolver } from "../../../domain/credentials/OAuth2RedirectUriResolver";
|
|
11
|
+
import { HttpRequestJsonBodyReader } from "../HttpRequestJsonBodyReader";
|
|
5
12
|
import { ServerHttpErrorResponseFactory } from "../ServerHttpErrorResponseFactory";
|
|
6
13
|
|
|
14
|
+
type OAuthStartRequestBody = Readonly<{
|
|
15
|
+
typeId: string;
|
|
16
|
+
instanceId: string;
|
|
17
|
+
redirectUri: string;
|
|
18
|
+
scopes?: ReadonlyArray<string>;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
7
21
|
@injectable()
|
|
8
22
|
export class OAuth2HttpRouteHandler {
|
|
9
23
|
constructor(
|
|
10
|
-
@inject(
|
|
11
|
-
private readonly
|
|
24
|
+
@inject(OAuth2RedirectUriResolver)
|
|
25
|
+
private readonly redirectUriResolver: OAuth2RedirectUriResolver,
|
|
12
26
|
@inject(CredentialInstanceService)
|
|
13
27
|
private readonly credentialInstanceService: CredentialInstanceService,
|
|
28
|
+
@inject(ApplicationTokens.OAuthFlowExecutor)
|
|
29
|
+
private readonly oauthFlowExecutor: OAuthFlowExecutor,
|
|
30
|
+
@inject(ApplicationTokens.CredentialStore)
|
|
31
|
+
private readonly credentialStore: CredentialStore,
|
|
32
|
+
@inject(CredentialSecretCipher)
|
|
33
|
+
private readonly credentialSecretCipher: CredentialSecretCipher,
|
|
14
34
|
) {}
|
|
15
35
|
|
|
16
|
-
async
|
|
36
|
+
async getRedirectUri(request: Request): Promise<Response> {
|
|
17
37
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return Response.json({ error: "Missing instanceId query parameter." }, { status: 400 });
|
|
22
|
-
}
|
|
23
|
-
const redirect = await this.oauth2ConnectService.createAuthRedirect(
|
|
24
|
-
instanceId,
|
|
25
|
-
this.resolveRequestOrigin(request),
|
|
26
|
-
);
|
|
27
|
-
return Response.redirect(redirect.redirectUrl, 302);
|
|
38
|
+
return Response.json({
|
|
39
|
+
redirectUri: this.redirectUriResolver.resolve(this.resolveRequestOrigin(request)),
|
|
40
|
+
});
|
|
28
41
|
} catch (error) {
|
|
29
42
|
return ServerHttpErrorResponseFactory.fromUnknown(error);
|
|
30
43
|
}
|
|
31
44
|
}
|
|
32
45
|
|
|
33
|
-
async
|
|
46
|
+
async postDisconnect(request: Request): Promise<Response> {
|
|
34
47
|
try {
|
|
35
48
|
const url = new URL(request.url);
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return new Response(this.createPopupHtml({ kind: "oauth2.connected", ...result }), {
|
|
42
|
-
headers: {
|
|
43
|
-
"content-type": "text/html; charset=utf-8",
|
|
44
|
-
},
|
|
45
|
-
});
|
|
49
|
+
const instanceId = url.searchParams.get("instanceId")?.trim();
|
|
50
|
+
if (!instanceId) {
|
|
51
|
+
return Response.json({ error: "Missing instanceId query parameter." }, { status: 400 });
|
|
52
|
+
}
|
|
53
|
+
return Response.json(await this.credentialInstanceService.disconnectOAuth2(instanceId));
|
|
46
54
|
} catch (error) {
|
|
47
|
-
|
|
48
|
-
return new Response(this.createPopupHtml({ kind: "oauth2.error", message }), {
|
|
49
|
-
status: 400,
|
|
50
|
-
headers: {
|
|
51
|
-
"content-type": "text/html; charset=utf-8",
|
|
52
|
-
},
|
|
53
|
-
});
|
|
55
|
+
return ServerHttpErrorResponseFactory.fromUnknown(error);
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
async
|
|
59
|
+
async postOAuthStart(request: Request): Promise<Response> {
|
|
58
60
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
const body = await HttpRequestJsonBodyReader.readJsonBody<OAuthStartRequestBody>(request);
|
|
62
|
+
if (!body.typeId?.trim()) {
|
|
63
|
+
return Response.json({ error: "Missing required field: typeId" }, { status: 400 });
|
|
64
|
+
}
|
|
65
|
+
if (!body.instanceId?.trim()) {
|
|
66
|
+
return Response.json({ error: "Missing required field: instanceId" }, { status: 400 });
|
|
67
|
+
}
|
|
68
|
+
if (!body.redirectUri?.trim()) {
|
|
69
|
+
return Response.json({ error: "Missing required field: redirectUri" }, { status: 400 });
|
|
70
|
+
}
|
|
71
|
+
const result = await this.oauthFlowExecutor.start({
|
|
72
|
+
typeId: body.typeId.trim(),
|
|
73
|
+
instanceId: body.instanceId.trim(),
|
|
74
|
+
redirectUri: body.redirectUri.trim(),
|
|
75
|
+
scopes: body.scopes ?? [],
|
|
61
76
|
});
|
|
77
|
+
return Response.json({ consentUrl: result.consentUrl, stateToken: result.stateToken });
|
|
62
78
|
} catch (error) {
|
|
63
79
|
return ServerHttpErrorResponseFactory.fromUnknown(error);
|
|
64
80
|
}
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
async
|
|
83
|
+
async getOAuthCallback(request: Request): Promise<Response> {
|
|
68
84
|
try {
|
|
69
85
|
const url = new URL(request.url);
|
|
70
|
-
const
|
|
86
|
+
const code = url.searchParams.get("code")?.trim();
|
|
87
|
+
const stateToken = url.searchParams.get("state")?.trim();
|
|
88
|
+
if (!code || !stateToken) {
|
|
89
|
+
return new Response(
|
|
90
|
+
this.createPopupHtml({ kind: "oauth2.error", message: "Missing code or state parameter." }),
|
|
91
|
+
{
|
|
92
|
+
status: 400,
|
|
93
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const instanceId = this.oauthFlowExecutor.lookupInstanceId(stateToken);
|
|
71
98
|
if (!instanceId) {
|
|
72
|
-
return Response
|
|
99
|
+
return new Response(
|
|
100
|
+
this.createPopupHtml({ kind: "oauth2.error", message: "OAuth state token not found or already used." }),
|
|
101
|
+
{ status: 400, headers: { "content-type": "text/html; charset=utf-8" } },
|
|
102
|
+
);
|
|
73
103
|
}
|
|
74
|
-
|
|
104
|
+
const material = await this.oauthFlowExecutor.completeCallback({ stateToken, code });
|
|
105
|
+
const nowIso = new Date().toISOString();
|
|
106
|
+
const encryptedMaterial = this.credentialSecretCipher.encrypt({
|
|
107
|
+
accessToken: material.accessToken,
|
|
108
|
+
refreshToken: material.refreshToken ?? null,
|
|
109
|
+
expiresAt: material.expiresAt ?? null,
|
|
110
|
+
grantedScopes: material.grantedScopes.join(" "),
|
|
111
|
+
});
|
|
112
|
+
await this.credentialStore.saveOAuth2Material({
|
|
113
|
+
instanceId,
|
|
114
|
+
encryptedJson: encryptedMaterial.encryptedJson,
|
|
115
|
+
encryptionKeyId: encryptedMaterial.encryptionKeyId,
|
|
116
|
+
schemaVersion: encryptedMaterial.schemaVersion,
|
|
117
|
+
metadata: {
|
|
118
|
+
providerId: "local",
|
|
119
|
+
connectedAt: nowIso,
|
|
120
|
+
scopes: [...material.grantedScopes],
|
|
121
|
+
updatedAt: nowIso,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
await this.credentialInstanceService.markOAuth2Connected(instanceId, nowIso);
|
|
125
|
+
return new Response(this.createPopupHtml({ kind: "oauth2.connected", instanceId }), {
|
|
126
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
127
|
+
});
|
|
75
128
|
} catch (error) {
|
|
76
|
-
|
|
129
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
130
|
+
return new Response(this.createPopupHtml({ kind: "oauth2.error", message }), {
|
|
131
|
+
status: 400,
|
|
132
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
133
|
+
});
|
|
77
134
|
}
|
|
78
135
|
}
|
|
79
136
|
|