@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
|
@@ -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
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createServer, type Server } from "node:http";
|
|
2
|
+
import type { Logger } from "../../application/logging/Logger";
|
|
3
|
+
import type { CodemationHonoApiApp } from "./hono/CodemationHonoApiAppFactory";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a Node.js http.Server that bridges IncomingMessage to Hono's Fetch API.
|
|
7
|
+
* Used by {@link import("../../bootstrap/runtime/HeadlessApiRuntime").HeadlessApiRuntime}
|
|
8
|
+
* to serve the Hono API without Next.js.
|
|
9
|
+
*/
|
|
10
|
+
export class HeadlessHttpServerFactory {
|
|
11
|
+
create(honoApp: CodemationHonoApiApp, port: number, logger: Logger): Server {
|
|
12
|
+
return createServer((req, res) => {
|
|
13
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? `127.0.0.1:${port}`}`);
|
|
14
|
+
const headers = new Headers();
|
|
15
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
16
|
+
if (value === undefined) continue;
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
for (const v of value) headers.append(key, v);
|
|
19
|
+
} else {
|
|
20
|
+
headers.set(key, value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const chunks: Buffer[] = [];
|
|
24
|
+
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
25
|
+
req.on("end", () => {
|
|
26
|
+
// eslint-disable-next-line codemation/no-buffer-everything -- node:http bridge; no streaming alternative when adapting IncomingMessage to Fetch API Request
|
|
27
|
+
const body = chunks.length > 0 ? Buffer.concat(chunks) : null;
|
|
28
|
+
const fetchRequest = new Request(url, {
|
|
29
|
+
method: req.method ?? "GET",
|
|
30
|
+
headers,
|
|
31
|
+
body: body?.byteLength ? body : undefined,
|
|
32
|
+
// @ts-expect-error — Node's Request needs duplex for streaming; required in some runtimes
|
|
33
|
+
duplex: "half",
|
|
34
|
+
});
|
|
35
|
+
Promise.resolve(honoApp.fetch(fetchRequest))
|
|
36
|
+
.then(async (fetchResponse: Response) => {
|
|
37
|
+
const responseHeaders: Record<string, string> = {};
|
|
38
|
+
fetchResponse.headers.forEach((value, key) => {
|
|
39
|
+
responseHeaders[key] = value;
|
|
40
|
+
});
|
|
41
|
+
res.writeHead(fetchResponse.status, responseHeaders);
|
|
42
|
+
// eslint-disable-next-line codemation/no-buffer-everything -- node:http bridge; Hono Fetch Response must be fully buffered to write to ServerResponse
|
|
43
|
+
const responseBody = await fetchResponse.arrayBuffer();
|
|
44
|
+
res.end(Buffer.from(responseBody));
|
|
45
|
+
})
|
|
46
|
+
.catch((err: unknown) => {
|
|
47
|
+
logger.error("Unhandled request error", err instanceof Error ? err : new Error(String(err)));
|
|
48
|
+
if (!res.headersSent) {
|
|
49
|
+
res.writeHead(500);
|
|
50
|
+
res.end("Internal server error");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -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
|
|
|
@@ -6,6 +6,7 @@ import type { NamespacedUnregister } from "tsx/esm/api";
|
|
|
6
6
|
import type { CodemationConfig } from "../config/CodemationConfig";
|
|
7
7
|
import { CodemationConfigNormalizer } from "../config/CodemationConfigNormalizer";
|
|
8
8
|
import type { NormalizedCodemationConfig } from "../config/CodemationConfigNormalizer";
|
|
9
|
+
import { BootTimer } from "../../bootstrap/perf/BootTimer";
|
|
9
10
|
import { logLevelPolicyFactory } from "../../infrastructure/logging/LogLevelPolicyFactory";
|
|
10
11
|
import { ServerLoggerFactory } from "../../infrastructure/logging/ServerLoggerFactory";
|
|
11
12
|
import { DiscoveredWorkflowsEmptyMessageFactory } from "./DiscoveredWorkflowsEmptyMessageFactory";
|
|
@@ -37,9 +38,46 @@ export class CodemationConsumerConfigLoader {
|
|
|
37
38
|
private readonly performanceDiagnosticsLogger = new ServerLoggerFactory(
|
|
38
39
|
logLevelPolicyFactory,
|
|
39
40
|
).createPerformanceDiagnostics("codemation-config-loader.timing");
|
|
41
|
+
private readonly bootLogger = new ServerLoggerFactory(logLevelPolicyFactory).create("codemation.boot");
|
|
42
|
+
/**
|
|
43
|
+
* In-flight + completed load promises keyed by `${consumerRoot}|${configPathOverride}`. The
|
|
44
|
+
* boot path constructs MULTIPLE CodemationConsumerConfigLoader instances (one inside the CLI's
|
|
45
|
+
* DatabaseMigrationsApplyService, another inside NextHostEdgeSeedLoader, another inside
|
|
46
|
+
* AppConfigLoader for the disposable runtime) and each independently calls `load(...)`. Without
|
|
47
|
+
* a cache shared across instances, the same `${consumerRoot}` ends up importing
|
|
48
|
+
* codemation.config.ts + discovered workflow modules ~3 times for a single dev boot. The cache
|
|
49
|
+
* has to be static so it spans every loader instance in the process.
|
|
50
|
+
*
|
|
51
|
+
* Callers MUST invoke `invalidateAll()` on a source-change reload — the dev source watcher
|
|
52
|
+
* already tears the runtime down and reboots; it just needs to clear this map first.
|
|
53
|
+
*/
|
|
54
|
+
private static readonly resolutionCache = new Map<string, Promise<CodemationConsumerConfigResolution>>();
|
|
55
|
+
|
|
56
|
+
static invalidateAll(): void {
|
|
57
|
+
this.resolutionCache.clear();
|
|
58
|
+
}
|
|
40
59
|
|
|
41
60
|
async load(
|
|
42
61
|
args: Readonly<{ consumerRoot: string; configPathOverride?: string }>,
|
|
62
|
+
): Promise<CodemationConsumerConfigResolution> {
|
|
63
|
+
const cacheKey = `${args.consumerRoot}|${args.configPathOverride ?? ""}`;
|
|
64
|
+
const cached = CodemationConsumerConfigLoader.resolutionCache.get(cacheKey);
|
|
65
|
+
if (cached) {
|
|
66
|
+
return cached;
|
|
67
|
+
}
|
|
68
|
+
const promise = this.loadUncached(args);
|
|
69
|
+
CodemationConsumerConfigLoader.resolutionCache.set(cacheKey, promise);
|
|
70
|
+
try {
|
|
71
|
+
return await promise;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// A failed load shouldn't poison the cache — future retries should re-attempt.
|
|
74
|
+
CodemationConsumerConfigLoader.resolutionCache.delete(cacheKey);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async loadUncached(
|
|
80
|
+
args: Readonly<{ consumerRoot: string; configPathOverride?: string }>,
|
|
43
81
|
): Promise<CodemationConsumerConfigResolution> {
|
|
44
82
|
const loadStarted = performance.now();
|
|
45
83
|
let mark = loadStarted;
|
|
@@ -54,25 +92,36 @@ export class CodemationConsumerConfigLoader {
|
|
|
54
92
|
`load.${label} +${delta.toFixed(1)}ms (cumulative ${(now - loadStarted).toFixed(1)}ms)`,
|
|
55
93
|
);
|
|
56
94
|
};
|
|
57
|
-
const bootstrapSource = await
|
|
95
|
+
const bootstrapSource = await BootTimer.measureAsync("config.resolveConfigPath", () =>
|
|
96
|
+
this.resolveConfigPath(args.consumerRoot, args.configPathOverride),
|
|
97
|
+
);
|
|
58
98
|
phaseMs("resolveConfigPath");
|
|
59
99
|
if (!bootstrapSource) {
|
|
60
100
|
throw new Error(
|
|
61
101
|
'Codemation config not found. Expected "codemation.config.ts" in the consumer project root or "src/".',
|
|
62
102
|
);
|
|
63
103
|
}
|
|
64
|
-
const moduleExports = await
|
|
104
|
+
const moduleExports = await BootTimer.measureAsync("config.importConfigModule", () =>
|
|
105
|
+
this.importModule(bootstrapSource, importSession),
|
|
106
|
+
);
|
|
65
107
|
phaseMs("importConfigModule");
|
|
66
108
|
const rawConfig = this.configExportsResolver.resolveConfig(moduleExports);
|
|
67
109
|
if (!rawConfig) {
|
|
68
110
|
throw new Error(`Config file does not export a Codemation config object: ${bootstrapSource}`);
|
|
69
111
|
}
|
|
70
112
|
const config = this.configNormalizer.normalize(rawConfig);
|
|
71
|
-
|
|
113
|
+
if (rawConfig.codemationVersion) {
|
|
114
|
+
this.bootLogger.info(`codemationVersion: ${rawConfig.codemationVersion}`);
|
|
115
|
+
}
|
|
116
|
+
const workflowSources = await BootTimer.measureAsync("config.resolveWorkflowSources", () =>
|
|
117
|
+
this.resolveWorkflowSources(args.consumerRoot, config),
|
|
118
|
+
);
|
|
72
119
|
phaseMs("resolveWorkflowSources");
|
|
73
|
-
const workflows =
|
|
74
|
-
|
|
75
|
-
|
|
120
|
+
const workflows = await BootTimer.measureAsync("config.loadDiscoveredWorkflows", async () =>
|
|
121
|
+
this.mergeWorkflows(
|
|
122
|
+
config.workflows ?? [],
|
|
123
|
+
await this.loadDiscoveredWorkflows(args.consumerRoot, config, workflowSources, importSession),
|
|
124
|
+
),
|
|
76
125
|
);
|
|
77
126
|
phaseMs("loadDiscoveredWorkflows");
|
|
78
127
|
const resolvedConfig: NormalizedCodemationConfig = {
|
|
@@ -145,7 +194,10 @@ export class CodemationConsumerConfigLoader {
|
|
|
145
194
|
workflowDiscoveryDirectories,
|
|
146
195
|
absoluteWorkflowModulePath: workflowSource,
|
|
147
196
|
}),
|
|
148
|
-
moduleExports: await
|
|
197
|
+
moduleExports: await BootTimer.measureAsync(
|
|
198
|
+
`workflow.${path.basename(workflowSource).replace(/\.tsx?$/, "")}`,
|
|
199
|
+
() => this.importModule(workflowSource, importSession),
|
|
200
|
+
),
|
|
149
201
|
})),
|
|
150
202
|
);
|
|
151
203
|
for (const loadedWorkflowModule of loadedWorkflowModules) {
|
|
@@ -124,6 +124,7 @@ export class CodemationPluginDiscovery {
|
|
|
124
124
|
}
|
|
125
125
|
const pluginValue = value as {
|
|
126
126
|
credentialTypes?: unknown;
|
|
127
|
+
mcpServers?: unknown;
|
|
127
128
|
register?: unknown;
|
|
128
129
|
sandbox?: unknown;
|
|
129
130
|
};
|
|
@@ -133,9 +134,13 @@ export class CodemationPluginDiscovery {
|
|
|
133
134
|
if (pluginValue.credentialTypes !== undefined && !Array.isArray(pluginValue.credentialTypes)) {
|
|
134
135
|
return false;
|
|
135
136
|
}
|
|
137
|
+
if (pluginValue.mcpServers !== undefined && !Array.isArray(pluginValue.mcpServers)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
136
140
|
return (
|
|
137
141
|
pluginValue.register !== undefined ||
|
|
138
142
|
pluginValue.credentialTypes !== undefined ||
|
|
143
|
+
pluginValue.mcpServers !== undefined ||
|
|
139
144
|
pluginValue.sandbox !== undefined ||
|
|
140
145
|
Object.keys(pluginValue).length === 0
|
|
141
146
|
);
|