@codemation/host 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/{AppConfigFactory-BiFHnorf.d.ts → AppConfigFactory-DHdAGOmC.d.ts} +3 -3
  3. package/dist/{AppContainerFactory-BRU02PTm.js → AppContainerFactory-BKaAUIi-.js} +91 -27
  4. package/dist/AppContainerFactory-BKaAUIi-.js.map +1 -0
  5. package/dist/{CodemationConfig-DuGk7uN5.d.ts → CodemationConfig-DBbMU3HB.d.ts} +2 -2
  6. package/dist/{CodemationConfigNormalizer-BP2-0ZDE.d.ts → CodemationConfigNormalizer-C8wC0skq.d.ts} +2 -2
  7. package/dist/{CodemationConsumerConfigLoader-D5CSz3TQ.d.ts → CodemationConsumerConfigLoader-Ceh6sB3X.d.ts} +3 -2
  8. package/dist/{CodemationConsumerConfigLoader-C_ISRrpI.js → CodemationConsumerConfigLoader-D6LFSlp5.js} +20 -7
  9. package/dist/CodemationConsumerConfigLoader-D6LFSlp5.js.map +1 -0
  10. package/dist/{CodemationPluginListMerger-DFzGgfyI.d.ts → CodemationPluginListMerger-Cx9DnyR-.d.ts} +5 -5
  11. package/dist/{CredentialServices-Bhejvys-.d.ts → CredentialServices-CYETzKyb.d.ts} +14 -5
  12. package/dist/{CredentialServices-xVxVA9Tq.js → CredentialServices-D8BBZH1i.js} +40 -6
  13. package/dist/CredentialServices-D8BBZH1i.js.map +1 -0
  14. package/dist/{PublicFrontendBootstrapFactory-CSgWyTra.d.ts → PublicFrontendBootstrapFactory-DeMjp3cs.d.ts} +5 -2
  15. package/dist/consumer.d.ts +4 -4
  16. package/dist/consumer.js +1 -1
  17. package/dist/credentials.d.ts +3 -3
  18. package/dist/credentials.js +1 -1
  19. package/dist/devServerSidecar.d.ts +1 -1
  20. package/dist/{index-BQaZZmOm.d.ts → index-dK05sTQ4.d.ts} +50 -60
  21. package/dist/index.d.ts +97 -12
  22. package/dist/index.js +4 -4
  23. package/dist/nextServer.d.ts +16 -8
  24. package/dist/nextServer.js +2 -2
  25. package/dist/{persistenceServer-fdldtXJH.d.ts → persistenceServer-D9vUTMqc.d.ts} +2 -2
  26. package/dist/persistenceServer.d.ts +5 -5
  27. package/dist/{server-ChTCEc6R.js → server-C-WZcsId.js} +5 -6
  28. package/dist/{server-ChTCEc6R.js.map → server-C-WZcsId.js.map} +1 -1
  29. package/dist/{server-Cpzpy1Ar.d.ts → server-fxqY2WEi.d.ts} +5 -5
  30. package/dist/server.d.ts +8 -8
  31. package/dist/server.js +4 -4
  32. package/package.json +6 -5
  33. package/playwright.config.ts +10 -6
  34. package/src/application/contracts/WorkflowViewContracts.ts +3 -0
  35. package/src/application/mapping/WorkflowDefinitionMapper.ts +117 -19
  36. package/src/bootstrap/AppContainerFactory.ts +2 -0
  37. package/src/domain/credentials/CredentialInstanceService.ts +8 -1
  38. package/src/domain/credentials/CredentialOAuth2ScopeResolver.ts +61 -0
  39. package/src/domain/credentials/CredentialServices.ts +1 -0
  40. package/src/domain/credentials/OAuth2ConnectServiceFactory.ts +9 -2
  41. package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +2 -1
  42. package/src/presentation/http/routeHandlers/WorkflowHttpRouteHandler.ts +4 -2
  43. package/src/presentation/server/CodemationConsumerConfigLoader.ts +28 -5
  44. package/src/presentation/server/CodemationPluginDiscovery.ts +4 -6
  45. package/dist/AppContainerFactory-BRU02PTm.js.map +0 -1
  46. package/dist/CodemationConsumerConfigLoader-C_ISRrpI.js.map +0 -1
  47. package/dist/CredentialServices-xVxVA9Tq.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"server-ChTCEc6R.js","names":["config: CodemationConfig","consumerRoot: string","configSource?: string","workflowSources: ReadonlyArray<string>","env?: Readonly<NodeJS.ProcessEnv>","consumerConfigLoader: CodemationConsumerConfigLoader","appConfigFactory: AppConfigFactory","discoveredPackages: CodemationDiscoveredPluginPackage[]","resolvedPackages: CodemationResolvedPluginPackage[]","packageRoots: string[]"],"sources":["../src/presentation/http/CodemationServerGatewayFactory.ts","../src/presentation/server/AppConfigLoader.ts","../src/presentation/server/CodemationPluginDiscovery.ts"],"sourcesContent":["import { accessSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { QueryBus } from \"../../application/bus/QueryBus\";\nimport type { WorkflowDto, WorkflowSummary } from \"../../application/contracts/WorkflowViewContracts\";\nimport { WorkflowDefinitionMapper } from \"../../application/mapping/WorkflowDefinitionMapper\";\nimport { GetWorkflowDetailQuery } from \"../../application/queries/GetWorkflowDetailQuery\";\nimport { GetWorkflowSummariesQuery } from \"../../application/queries/GetWorkflowSummariesQuery\";\nimport { ApplicationTokens } from \"../../applicationTokens\";\nimport { AppContainerFactory } from \"../../bootstrap/AppContainerFactory\";\nimport { AppContainerLifecycle } from \"../../bootstrap/AppContainerLifecycle\";\nimport { FrontendRuntime } from \"../../bootstrap/runtime/FrontendRuntime\";\nimport { AppConfigFactory } from \"../../bootstrap/runtime/AppConfigFactory\";\nimport type { CodemationConfig } from \"../config/CodemationConfig\";\nimport { CodemationConfigNormalizer } from \"../config/CodemationConfigNormalizer\";\nimport { CodemationHonoApiApp } from \"./hono/CodemationHonoApiAppFactory\";\n\ntype ServerGatewayContext = Readonly<{\n container: import(\"@codemation/core\").Container;\n httpApi: CodemationHonoApiApp;\n queryBus: QueryBus;\n workflowDefinitionMapper: WorkflowDefinitionMapper;\n}>;\n\nexport class CodemationServerGateway {\n private static readonly contextsByConfig = new WeakMap<object, Promise<ServerGatewayContext>>();\n\n constructor(\n private readonly config: CodemationConfig,\n private readonly consumerRoot: string,\n private readonly configSource?: string,\n private readonly workflowSources: ReadonlyArray<string> = [],\n private readonly env?: Readonly<NodeJS.ProcessEnv>,\n ) {}\n\n async dispatch(request: Request): Promise<Response> {\n return await (await this.getContext()).httpApi.fetch(request);\n }\n\n async prepare(): Promise<void> {\n await this.getContext();\n }\n\n async close(): Promise<void> {\n const cachedContext = CodemationServerGateway.contextsByConfig.get(this.config as object);\n if (!cachedContext) {\n return;\n }\n CodemationServerGateway.contextsByConfig.delete(this.config as object);\n await (await cachedContext).container.resolve(AppContainerLifecycle).stop();\n }\n\n async loadWorkflowSummaries(): Promise<ReadonlyArray<WorkflowSummary>> {\n const context = await this.getContext();\n const workflows = await context.queryBus.execute(new GetWorkflowSummariesQuery());\n return workflows.map((workflow) => context.workflowDefinitionMapper.toSummary(workflow));\n }\n\n async loadWorkflowDetail(workflowId: string): Promise<WorkflowDto> {\n const context = await this.getContext();\n const workflow = await context.queryBus.execute(new GetWorkflowDetailQuery(workflowId));\n if (!workflow) {\n throw new Error(`Unknown workflowId: ${workflowId}`);\n }\n return await context.workflowDefinitionMapper.map(workflow);\n }\n\n private getContext(): Promise<ServerGatewayContext> {\n const cachedContext = CodemationServerGateway.contextsByConfig.get(this.config as object);\n if (cachedContext) {\n return cachedContext;\n }\n const nextContext = this.createContext();\n CodemationServerGateway.contextsByConfig.set(this.config as object, nextContext);\n return nextContext;\n }\n\n private async createContext(): Promise<ServerGatewayContext> {\n const repoRoot = this.detectWorkspaceRoot(this.consumerRoot);\n // This gateway is the config/env boundary that materializes AppConfig from raw inputs.\n // eslint-disable-next-line no-restricted-properties\n const env = this.env ?? process.env;\n const appConfig = new AppConfigFactory().create({\n repoRoot,\n consumerRoot: this.consumerRoot,\n env,\n config: new CodemationConfigNormalizer().normalize(this.config),\n workflowSources: this.resolveWorkflowSources(),\n });\n const container = await new AppContainerFactory().create({\n appConfig,\n sharedWorkflowWebsocketServer: null,\n });\n await container.resolve(FrontendRuntime).start();\n return {\n container,\n httpApi: container.resolve(CodemationHonoApiApp),\n queryBus: container.resolve(ApplicationTokens.QueryBus),\n workflowDefinitionMapper: container.resolve(WorkflowDefinitionMapper),\n };\n }\n\n private resolveWorkflowSources(): ReadonlyArray<string> {\n if (this.workflowSources.length > 0) {\n return [...this.workflowSources];\n }\n if (!this.configSource || !this.config.workflows || this.config.workflows.length === 0) {\n return [];\n }\n return [this.configSource];\n }\n private detectWorkspaceRoot(startDirectory: string): string {\n let currentDirectory = path.resolve(startDirectory);\n while (true) {\n try {\n accessSync(path.resolve(currentDirectory, \"pnpm-workspace.yaml\"));\n return currentDirectory;\n } catch {\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return startDirectory;\n }\n currentDirectory = parentDirectory;\n }\n }\n }\n}\n","import type { AppConfig } from \"../config/AppConfig\";\nimport { CodemationConsumerConfigLoader } from \"./CodemationConsumerConfigLoader\";\nimport { AppConfigFactory } from \"../../bootstrap/runtime/AppConfigFactory\";\n\nexport type AppConfigLoadResult = Readonly<{\n appConfig: AppConfig;\n bootstrapSource: string | null;\n}>;\n\nexport class AppConfigLoader {\n constructor(\n private readonly consumerConfigLoader: CodemationConsumerConfigLoader = new CodemationConsumerConfigLoader(),\n private readonly appConfigFactory: AppConfigFactory = new AppConfigFactory(),\n ) {}\n\n async load(\n args: Readonly<{\n consumerRoot: string;\n repoRoot: string;\n env: NodeJS.ProcessEnv;\n configPathOverride?: string;\n }>,\n ): Promise<AppConfigLoadResult> {\n const resolution = await this.consumerConfigLoader.load({\n consumerRoot: args.consumerRoot,\n configPathOverride: args.configPathOverride,\n });\n return {\n appConfig: this.appConfigFactory.create({\n repoRoot: args.repoRoot,\n consumerRoot: args.consumerRoot,\n env: args.env,\n config: resolution.config,\n workflowSources: resolution.workflowSources,\n }),\n bootstrapSource: resolution.bootstrapSource,\n };\n }\n}\n","import type { CodemationPackageManifest } from \"../config/CodemationPackageManifest\";\nimport { CodemationPluginPackageMetadata, type CodemationPlugin } from \"../config/CodemationPlugin\";\nimport { readFile, readdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nexport type CodemationDiscoveredPluginPackage = Readonly<{\n packageName: string;\n packageRoot: string;\n pluginEntry: string;\n developmentEntry?: string;\n}>;\n\nexport type CodemationResolvedPluginPackage = Readonly<\n CodemationDiscoveredPluginPackage & {\n plugin: CodemationPlugin;\n }\n>;\n\ntype PackageJsonShape = Readonly<{\n codemation?: CodemationPackageManifest;\n name?: string;\n exports?: Readonly<Record<string, unknown>>;\n}>;\n\nexport class CodemationPluginDiscovery {\n private readonly pluginPackageMetadata = new CodemationPluginPackageMetadata();\n\n async discover(consumerRoot: string): Promise<ReadonlyArray<CodemationDiscoveredPluginPackage>> {\n const nodeModulesRoot = path.resolve(consumerRoot, \"node_modules\");\n const packageRoots = await this.collectPackageRoots(nodeModulesRoot);\n const discoveredPackages: CodemationDiscoveredPluginPackage[] = [];\n for (const packageRoot of packageRoots) {\n const packageJson = await this.readPackageJson(path.resolve(packageRoot, \"package.json\"));\n const pluginManifest = packageJson.codemation?.plugin;\n if (!packageJson.name || typeof pluginManifest !== \"string\" || pluginManifest.trim().length === 0) {\n continue;\n }\n discoveredPackages.push({\n packageName: packageJson.name,\n packageRoot,\n pluginEntry: pluginManifest,\n developmentEntry: await this.resolveDevelopmentPluginEntry(packageRoot),\n });\n }\n return discoveredPackages.sort((left, right) => left.packageName.localeCompare(right.packageName));\n }\n\n async resolvePlugins(consumerRoot: string): Promise<ReadonlyArray<CodemationResolvedPluginPackage>> {\n const discoveredPackages = await this.discover(consumerRoot);\n return await this.resolveDiscoveredPackages(discoveredPackages);\n }\n\n async resolveDiscoveredPackages(\n discoveredPackages: ReadonlyArray<CodemationDiscoveredPluginPackage>,\n ): Promise<ReadonlyArray<CodemationResolvedPluginPackage>> {\n const resolvedPackages: CodemationResolvedPluginPackage[] = [];\n for (const discoveredPackage of discoveredPackages) {\n resolvedPackages.push({\n ...discoveredPackage,\n plugin: await this.loadPlugin(discoveredPackage),\n });\n }\n return resolvedPackages;\n }\n\n private async collectPackageRoots(nodeModulesRoot: string): Promise<ReadonlyArray<string>> {\n try {\n const entries = await readdir(nodeModulesRoot, { withFileTypes: true });\n const packageRoots: string[] = [];\n for (const entry of entries) {\n if (!entry.isDirectory() && !entry.isSymbolicLink()) {\n continue;\n }\n if (entry.name.startsWith(\"@\")) {\n const scopedEntries = await readdir(path.resolve(nodeModulesRoot, entry.name), { withFileTypes: true });\n for (const scopedEntry of scopedEntries) {\n if (scopedEntry.isDirectory() || scopedEntry.isSymbolicLink()) {\n packageRoots.push(path.resolve(nodeModulesRoot, entry.name, scopedEntry.name));\n }\n }\n continue;\n }\n packageRoots.push(path.resolve(nodeModulesRoot, entry.name));\n }\n return packageRoots;\n } catch {\n return [];\n }\n }\n\n private async readPackageJson(packageJsonPath: string): Promise<PackageJsonShape> {\n try {\n const rawPackageJson = await readFile(packageJsonPath, \"utf8\");\n return JSON.parse(rawPackageJson) as PackageJsonShape;\n } catch {\n return {};\n }\n }\n\n private async loadPlugin(discoveredPackage: CodemationDiscoveredPluginPackage): Promise<CodemationPlugin> {\n const pluginModulePath = path.resolve(discoveredPackage.packageRoot, this.resolvePluginEntry(discoveredPackage));\n const importedModule = (await import(\n /* webpackIgnore: true */ this.resolvePluginModuleSpecifier(pluginModulePath)\n )) as Record<string, unknown>;\n const exportedValue = importedModule.default;\n const plugin = this.resolvePluginValue(exportedValue);\n if (!plugin) {\n throw new Error(`Plugin package \"${discoveredPackage.packageName}\" did not default-export a Codemation plugin.`);\n }\n return this.pluginPackageMetadata.attachPackageName(plugin, discoveredPackage.packageName);\n }\n\n private resolvePluginValue(value: unknown): CodemationPlugin | null {\n if (this.isPluginConfig(value)) {\n return value;\n }\n return null;\n }\n\n private isPluginConfig(value: unknown): value is CodemationPlugin {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n const pluginValue = value as {\n credentialTypes?: unknown;\n register?: unknown;\n sandbox?: unknown;\n };\n if (pluginValue.register !== undefined && typeof pluginValue.register !== \"function\") {\n return false;\n }\n if (pluginValue.credentialTypes !== undefined && !Array.isArray(pluginValue.credentialTypes)) {\n return false;\n }\n return (\n pluginValue.register !== undefined ||\n pluginValue.credentialTypes !== undefined ||\n pluginValue.sandbox !== undefined ||\n Object.keys(pluginValue).length === 0\n );\n }\n\n private resolvePluginEntry(discoveredPackage: CodemationDiscoveredPluginPackage): string {\n if (\n process.env.CODEMATION_PREFER_PLUGIN_SOURCE_ENTRY === \"true\" &&\n typeof discoveredPackage.developmentEntry === \"string\" &&\n discoveredPackage.developmentEntry.trim().length > 0\n ) {\n return discoveredPackage.developmentEntry;\n }\n return discoveredPackage.pluginEntry;\n }\n\n private async resolveDevelopmentPluginEntry(packageRoot: string): Promise<string | undefined> {\n const candidates = [\n path.resolve(packageRoot, \"codemation.plugin.ts\"),\n path.resolve(packageRoot, \"codemation.plugin.js\"),\n path.resolve(packageRoot, \"src\", \"codemation.plugin.ts\"),\n path.resolve(packageRoot, \"src\", \"codemation.plugin.js\"),\n ];\n for (const candidate of candidates) {\n if (await this.exists(candidate)) {\n return path.relative(packageRoot, candidate);\n }\n }\n return undefined;\n }\n\n private async exists(filePath: string): Promise<boolean> {\n try {\n await readFile(filePath, \"utf8\");\n return true;\n } catch {\n return false;\n }\n }\n\n private resolvePluginModuleSpecifier(pluginModulePath: string): string {\n return pathToFileURL(pluginModulePath).href;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server-C-WZcsId.js","names":["config: CodemationConfig","consumerRoot: string","configSource?: string","workflowSources: ReadonlyArray<string>","env?: Readonly<NodeJS.ProcessEnv>","consumerConfigLoader: CodemationConsumerConfigLoader","appConfigFactory: AppConfigFactory","discoveredPackages: CodemationDiscoveredPluginPackage[]","resolvedPackages: CodemationResolvedPluginPackage[]","packageRoots: string[]"],"sources":["../src/presentation/http/CodemationServerGatewayFactory.ts","../src/presentation/server/AppConfigLoader.ts","../src/presentation/server/CodemationPluginDiscovery.ts"],"sourcesContent":["import { accessSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { QueryBus } from \"../../application/bus/QueryBus\";\nimport type { WorkflowDto, WorkflowSummary } from \"../../application/contracts/WorkflowViewContracts\";\nimport { WorkflowDefinitionMapper } from \"../../application/mapping/WorkflowDefinitionMapper\";\nimport { GetWorkflowDetailQuery } from \"../../application/queries/GetWorkflowDetailQuery\";\nimport { GetWorkflowSummariesQuery } from \"../../application/queries/GetWorkflowSummariesQuery\";\nimport { ApplicationTokens } from \"../../applicationTokens\";\nimport { AppContainerFactory } from \"../../bootstrap/AppContainerFactory\";\nimport { AppContainerLifecycle } from \"../../bootstrap/AppContainerLifecycle\";\nimport { FrontendRuntime } from \"../../bootstrap/runtime/FrontendRuntime\";\nimport { AppConfigFactory } from \"../../bootstrap/runtime/AppConfigFactory\";\nimport type { CodemationConfig } from \"../config/CodemationConfig\";\nimport { CodemationConfigNormalizer } from \"../config/CodemationConfigNormalizer\";\nimport { CodemationHonoApiApp } from \"./hono/CodemationHonoApiAppFactory\";\n\ntype ServerGatewayContext = Readonly<{\n container: import(\"@codemation/core\").Container;\n httpApi: CodemationHonoApiApp;\n queryBus: QueryBus;\n workflowDefinitionMapper: WorkflowDefinitionMapper;\n}>;\n\nexport class CodemationServerGateway {\n private static readonly contextsByConfig = new WeakMap<object, Promise<ServerGatewayContext>>();\n\n constructor(\n private readonly config: CodemationConfig,\n private readonly consumerRoot: string,\n private readonly configSource?: string,\n private readonly workflowSources: ReadonlyArray<string> = [],\n private readonly env?: Readonly<NodeJS.ProcessEnv>,\n ) {}\n\n async dispatch(request: Request): Promise<Response> {\n return await (await this.getContext()).httpApi.fetch(request);\n }\n\n async prepare(): Promise<void> {\n await this.getContext();\n }\n\n async close(): Promise<void> {\n const cachedContext = CodemationServerGateway.contextsByConfig.get(this.config as object);\n if (!cachedContext) {\n return;\n }\n CodemationServerGateway.contextsByConfig.delete(this.config as object);\n await (await cachedContext).container.resolve(AppContainerLifecycle).stop();\n }\n\n async loadWorkflowSummaries(): Promise<ReadonlyArray<WorkflowSummary>> {\n const context = await this.getContext();\n const workflows = await context.queryBus.execute(new GetWorkflowSummariesQuery());\n return workflows.map((workflow) => context.workflowDefinitionMapper.toSummary(workflow));\n }\n\n async loadWorkflowDetail(workflowId: string): Promise<WorkflowDto> {\n const context = await this.getContext();\n const workflow = await context.queryBus.execute(new GetWorkflowDetailQuery(workflowId));\n if (!workflow) {\n throw new Error(`Unknown workflowId: ${workflowId}`);\n }\n return await context.workflowDefinitionMapper.map(workflow);\n }\n\n private getContext(): Promise<ServerGatewayContext> {\n const cachedContext = CodemationServerGateway.contextsByConfig.get(this.config as object);\n if (cachedContext) {\n return cachedContext;\n }\n const nextContext = this.createContext();\n CodemationServerGateway.contextsByConfig.set(this.config as object, nextContext);\n return nextContext;\n }\n\n private async createContext(): Promise<ServerGatewayContext> {\n const repoRoot = this.detectWorkspaceRoot(this.consumerRoot);\n // This gateway is the config/env boundary that materializes AppConfig from raw inputs.\n // eslint-disable-next-line no-restricted-properties\n const env = this.env ?? process.env;\n const appConfig = new AppConfigFactory().create({\n repoRoot,\n consumerRoot: this.consumerRoot,\n env,\n config: new CodemationConfigNormalizer().normalize(this.config),\n workflowSources: this.resolveWorkflowSources(),\n });\n const container = await new AppContainerFactory().create({\n appConfig,\n sharedWorkflowWebsocketServer: null,\n });\n await container.resolve(FrontendRuntime).start();\n return {\n container,\n httpApi: container.resolve(CodemationHonoApiApp),\n queryBus: container.resolve(ApplicationTokens.QueryBus),\n workflowDefinitionMapper: container.resolve(WorkflowDefinitionMapper),\n };\n }\n\n private resolveWorkflowSources(): ReadonlyArray<string> {\n if (this.workflowSources.length > 0) {\n return [...this.workflowSources];\n }\n if (!this.configSource || !this.config.workflows || this.config.workflows.length === 0) {\n return [];\n }\n return [this.configSource];\n }\n private detectWorkspaceRoot(startDirectory: string): string {\n let currentDirectory = path.resolve(startDirectory);\n while (true) {\n try {\n accessSync(path.resolve(currentDirectory, \"pnpm-workspace.yaml\"));\n return currentDirectory;\n } catch {\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return startDirectory;\n }\n currentDirectory = parentDirectory;\n }\n }\n }\n}\n","import type { AppConfig } from \"../config/AppConfig\";\nimport { CodemationConsumerConfigLoader } from \"./CodemationConsumerConfigLoader\";\nimport { AppConfigFactory } from \"../../bootstrap/runtime/AppConfigFactory\";\n\nexport type AppConfigLoadResult = Readonly<{\n appConfig: AppConfig;\n bootstrapSource: string | null;\n}>;\n\nexport class AppConfigLoader {\n constructor(\n private readonly consumerConfigLoader: CodemationConsumerConfigLoader = new CodemationConsumerConfigLoader(),\n private readonly appConfigFactory: AppConfigFactory = new AppConfigFactory(),\n ) {}\n\n async load(\n args: Readonly<{\n consumerRoot: string;\n repoRoot: string;\n env: NodeJS.ProcessEnv;\n configPathOverride?: string;\n }>,\n ): Promise<AppConfigLoadResult> {\n const resolution = await this.consumerConfigLoader.load({\n consumerRoot: args.consumerRoot,\n configPathOverride: args.configPathOverride,\n });\n return {\n appConfig: this.appConfigFactory.create({\n repoRoot: args.repoRoot,\n consumerRoot: args.consumerRoot,\n env: args.env,\n config: resolution.config,\n workflowSources: resolution.workflowSources,\n }),\n bootstrapSource: resolution.bootstrapSource,\n };\n }\n}\n","import type { CodemationPackageManifest } from \"../config/CodemationPackageManifest\";\nimport { CodemationPluginPackageMetadata, type CodemationPlugin } from \"../config/CodemationPlugin\";\nimport { readFile, readdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nexport type CodemationDiscoveredPluginPackage = Readonly<{\n packageName: string;\n packageRoot: string;\n pluginEntry: string;\n developmentEntry?: string;\n}>;\n\nexport type CodemationResolvedPluginPackage = Readonly<\n CodemationDiscoveredPluginPackage & {\n plugin: CodemationPlugin;\n }\n>;\n\ntype PackageJsonShape = Readonly<{\n codemation?: CodemationPackageManifest;\n name?: string;\n exports?: Readonly<Record<string, unknown>>;\n}>;\n\nexport class CodemationPluginDiscovery {\n private readonly pluginPackageMetadata = new CodemationPluginPackageMetadata();\n\n async discover(consumerRoot: string): Promise<ReadonlyArray<CodemationDiscoveredPluginPackage>> {\n const nodeModulesRoot = path.resolve(consumerRoot, \"node_modules\");\n const packageRoots = await this.collectPackageRoots(nodeModulesRoot);\n const discoveredPackages: CodemationDiscoveredPluginPackage[] = [];\n for (const packageRoot of packageRoots) {\n const packageJson = await this.readPackageJson(path.resolve(packageRoot, \"package.json\"));\n const pluginManifest = packageJson.codemation?.plugin;\n if (!packageJson.name || typeof pluginManifest !== \"string\" || pluginManifest.trim().length === 0) {\n continue;\n }\n discoveredPackages.push({\n packageName: packageJson.name,\n packageRoot,\n pluginEntry: pluginManifest,\n developmentEntry: await this.resolveDevelopmentPluginEntry(packageRoot),\n });\n }\n return discoveredPackages.sort((left, right) => left.packageName.localeCompare(right.packageName));\n }\n\n async resolvePlugins(consumerRoot: string): Promise<ReadonlyArray<CodemationResolvedPluginPackage>> {\n const discoveredPackages = await this.discover(consumerRoot);\n return await this.resolveDiscoveredPackages(discoveredPackages);\n }\n\n async resolveDiscoveredPackages(\n discoveredPackages: ReadonlyArray<CodemationDiscoveredPluginPackage>,\n ): Promise<ReadonlyArray<CodemationResolvedPluginPackage>> {\n const resolvedPackages: CodemationResolvedPluginPackage[] = [];\n for (const discoveredPackage of discoveredPackages) {\n resolvedPackages.push({\n ...discoveredPackage,\n plugin: await this.loadPlugin(discoveredPackage),\n });\n }\n return resolvedPackages;\n }\n\n private async collectPackageRoots(nodeModulesRoot: string): Promise<ReadonlyArray<string>> {\n try {\n const entries = await readdir(nodeModulesRoot, { withFileTypes: true });\n const packageRoots: string[] = [];\n for (const entry of entries) {\n if (!entry.isDirectory() && !entry.isSymbolicLink()) {\n continue;\n }\n if (entry.name.startsWith(\"@\")) {\n const scopedEntries = await readdir(path.resolve(nodeModulesRoot, entry.name), { withFileTypes: true });\n for (const scopedEntry of scopedEntries) {\n if (scopedEntry.isDirectory() || scopedEntry.isSymbolicLink()) {\n packageRoots.push(path.resolve(nodeModulesRoot, entry.name, scopedEntry.name));\n }\n }\n continue;\n }\n packageRoots.push(path.resolve(nodeModulesRoot, entry.name));\n }\n return packageRoots;\n } catch {\n return [];\n }\n }\n\n private async readPackageJson(packageJsonPath: string): Promise<PackageJsonShape> {\n try {\n const rawPackageJson = await readFile(packageJsonPath, \"utf8\");\n return JSON.parse(rawPackageJson) as PackageJsonShape;\n } catch {\n return {};\n }\n }\n\n private async loadPlugin(discoveredPackage: CodemationDiscoveredPluginPackage): Promise<CodemationPlugin> {\n const pluginModulePath = path.resolve(discoveredPackage.packageRoot, this.resolvePluginEntry(discoveredPackage));\n const importedModule = (await import(\n /* webpackIgnore: true */ this.resolvePluginModuleSpecifier(pluginModulePath)\n )) as Record<string, unknown>;\n const exportedValue = importedModule.default;\n const plugin = this.resolvePluginValue(exportedValue);\n if (!plugin) {\n throw new Error(`Plugin package \"${discoveredPackage.packageName}\" did not default-export a Codemation plugin.`);\n }\n return this.pluginPackageMetadata.attachPackageName(plugin, discoveredPackage.packageName);\n }\n\n private resolvePluginValue(value: unknown): CodemationPlugin | null {\n if (this.isPluginConfig(value)) {\n return value;\n }\n return null;\n }\n\n private isPluginConfig(value: unknown): value is CodemationPlugin {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n const pluginValue = value as {\n credentialTypes?: unknown;\n register?: unknown;\n sandbox?: unknown;\n };\n if (pluginValue.register !== undefined && typeof pluginValue.register !== \"function\") {\n return false;\n }\n if (pluginValue.credentialTypes !== undefined && !Array.isArray(pluginValue.credentialTypes)) {\n return false;\n }\n return (\n pluginValue.register !== undefined ||\n pluginValue.credentialTypes !== undefined ||\n pluginValue.sandbox !== undefined ||\n Object.keys(pluginValue).length === 0\n );\n }\n\n private resolvePluginEntry(discoveredPackage: CodemationDiscoveredPluginPackage): string {\n const preferSource =\n process.env.CODEMATION_PREFER_PLUGIN_SOURCE_ENTRY === \"true\" &&\n typeof discoveredPackage.developmentEntry === \"string\" &&\n discoveredPackage.developmentEntry.trim().length > 0;\n const selectedEntry = preferSource ? discoveredPackage.developmentEntry : discoveredPackage.pluginEntry;\n return selectedEntry;\n }\n\n private async resolveDevelopmentPluginEntry(packageRoot: string): Promise<string | undefined> {\n const candidates = [\n path.resolve(packageRoot, \"codemation.plugin.ts\"),\n path.resolve(packageRoot, \"codemation.plugin.js\"),\n path.resolve(packageRoot, \"src\", \"codemation.plugin.ts\"),\n path.resolve(packageRoot, \"src\", \"codemation.plugin.js\"),\n ];\n for (const candidate of candidates) {\n if (await this.exists(candidate)) {\n return path.relative(packageRoot, candidate);\n }\n }\n return undefined;\n }\n\n private async exists(filePath: string): Promise<boolean> {\n try {\n await readFile(filePath, \"utf8\");\n return true;\n } catch {\n return false;\n }\n }\n\n private resolvePluginModuleSpecifier(pluginModulePath: string): string {\n return pathToFileURL(pluginModulePath).href;\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { a as CodemationConfig, g as AppConfig, y as CodemationPlugin } from "./CodemationConfig-DuGk7uN5.js";
2
- import { t as CodemationConsumerConfigLoader } from "./CodemationConsumerConfigLoader-D5CSz3TQ.js";
3
- import { t as AppConfigFactory } from "./AppConfigFactory-BiFHnorf.js";
4
- import { a as WorkflowDto, o as WorkflowSummary } from "./PublicFrontendBootstrapFactory-CSgWyTra.js";
1
+ import { a as CodemationConfig, g as AppConfig, y as CodemationPlugin } from "./CodemationConfig-DBbMU3HB.js";
2
+ import { t as CodemationConsumerConfigLoader } from "./CodemationConsumerConfigLoader-Ceh6sB3X.js";
3
+ import { t as AppConfigFactory } from "./AppConfigFactory-DHdAGOmC.js";
4
+ import { a as WorkflowDto, o as WorkflowSummary } from "./PublicFrontendBootstrapFactory-DeMjp3cs.js";
5
5
 
6
6
  //#region src/presentation/http/ApiPaths.d.ts
7
7
  declare class ApiPaths {
@@ -150,4 +150,4 @@ declare class WorkflowDiscoveryPathSegmentsComputer {
150
150
  }
151
151
  //#endregion
152
152
  export { CodemationResolvedPluginPackage as a, CodemationServerGateway as c, CodemationPluginDiscovery as i, ApiPaths as l, WorkflowModulePathFinder as n, AppConfigLoadResult as o, CodemationDiscoveredPluginPackage as r, AppConfigLoader as s, WorkflowDiscoveryPathSegmentsComputer as t };
153
- //# sourceMappingURL=server-Cpzpy1Ar.d.ts.map
153
+ //# sourceMappingURL=server-fxqY2WEi.d.ts.map
package/dist/server.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import "./CodemationAuthConfig-7hEfICPf.js";
2
2
  import { n as InternalAuthBootstrap, r as FrontendAppConfig, t as PublicFrontendBootstrap } from "./PublicFrontendBootstrap-DCniMBGu.js";
3
3
  import { i as CodemationFrontendAuthSnapshotJsonCodec, n as InternalAuthBootstrapJsonCodec, r as FrontendAppConfigJsonCodec, t as PublicFrontendBootstrapJsonCodec } from "./PublicFrontendBootstrapJsonCodec-BE0mhe1v.js";
4
- import "./index-BQaZZmOm.js";
5
- import "./CodemationConfig-DuGk7uN5.js";
6
- import "./CodemationConfigNormalizer-BP2-0ZDE.js";
7
- import { i as CodemationConsumerAppResolver, n as CodemationConsumerConfigResolution, r as CodemationConsumerApp, t as CodemationConsumerConfigLoader } from "./CodemationConsumerConfigLoader-D5CSz3TQ.js";
8
- import { r as PrismaDatabaseClient } from "./AppConfigFactory-BiFHnorf.js";
9
- import { t as CodemationPostgresPrismaClientFactory } from "./persistenceServer-fdldtXJH.js";
10
- import { a as CodemationResolvedPluginPackage, c as CodemationServerGateway, i as CodemationPluginDiscovery, l as ApiPaths, n as WorkflowModulePathFinder, o as AppConfigLoadResult, r as CodemationDiscoveredPluginPackage, s as AppConfigLoader, t as WorkflowDiscoveryPathSegmentsComputer } from "./server-Cpzpy1Ar.js";
11
- import { i as CodemationFrontendAuthSnapshotFactory, n as InternalAuthBootstrapFactory, r as FrontendAppConfigFactory, t as PublicFrontendBootstrapFactory } from "./PublicFrontendBootstrapFactory-CSgWyTra.js";
4
+ import "./index-dK05sTQ4.js";
5
+ import "./CodemationConfig-DBbMU3HB.js";
6
+ import "./CodemationConfigNormalizer-C8wC0skq.js";
7
+ import { i as CodemationConsumerAppResolver, n as CodemationConsumerConfigResolution, r as CodemationConsumerApp, t as CodemationConsumerConfigLoader } from "./CodemationConsumerConfigLoader-Ceh6sB3X.js";
8
+ import { r as PrismaDatabaseClient } from "./AppConfigFactory-DHdAGOmC.js";
9
+ import { t as CodemationPostgresPrismaClientFactory } from "./persistenceServer-D9vUTMqc.js";
10
+ import { a as CodemationResolvedPluginPackage, c as CodemationServerGateway, i as CodemationPluginDiscovery, l as ApiPaths, n as WorkflowModulePathFinder, o as AppConfigLoadResult, r as CodemationDiscoveredPluginPackage, s as AppConfigLoader, t as WorkflowDiscoveryPathSegmentsComputer } from "./server-fxqY2WEi.js";
11
+ import { i as CodemationFrontendAuthSnapshotFactory, n as InternalAuthBootstrapFactory, r as FrontendAppConfigFactory, t as PublicFrontendBootstrapFactory } from "./PublicFrontendBootstrapFactory-DeMjp3cs.js";
12
12
  export { ApiPaths, AppConfigLoadResult, AppConfigLoader, CodemationConsumerApp, CodemationConsumerAppResolver, CodemationConsumerConfigLoader, CodemationConsumerConfigResolution, CodemationDiscoveredPluginPackage, CodemationFrontendAuthSnapshotFactory, CodemationFrontendAuthSnapshotJsonCodec, CodemationPluginDiscovery, CodemationPostgresPrismaClientFactory, CodemationResolvedPluginPackage, CodemationServerGateway, FrontendAppConfig, FrontendAppConfigFactory, FrontendAppConfigJsonCodec, InternalAuthBootstrap, InternalAuthBootstrapFactory, InternalAuthBootstrapJsonCodec, PrismaDatabaseClient as PrismaClient, PublicFrontendBootstrap, PublicFrontendBootstrapFactory, PublicFrontendBootstrapJsonCodec, WorkflowDiscoveryPathSegmentsComputer, WorkflowModulePathFinder };
package/dist/server.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import "./ConsoleLogger-ClPU7jtc.js";
2
2
  import { i as CodemationFrontendAuthSnapshotJsonCodec, n as InternalAuthBootstrapJsonCodec, r as FrontendAppConfigJsonCodec, t as PublicFrontendBootstrapJsonCodec } from "./PublicFrontendBootstrapJsonCodec-BdiVGG5R.js";
3
- import { i as CodemationConsumerAppResolver, n as WorkflowDiscoveryPathSegmentsComputer, r as WorkflowModulePathFinder, t as CodemationConsumerConfigLoader } from "./CodemationConsumerConfigLoader-C_ISRrpI.js";
3
+ import { i as CodemationConsumerAppResolver, n as WorkflowDiscoveryPathSegmentsComputer, r as WorkflowModulePathFinder, t as CodemationConsumerConfigLoader } from "./CodemationConsumerConfigLoader-D6LFSlp5.js";
4
4
  import "./ServerLoggerFactory-BltIIDfQ.js";
5
5
  import "./decorateParam-DrsXNPuw.js";
6
6
  import "./decorate-B0PP651O.js";
7
- import "./CredentialServices-xVxVA9Tq.js";
8
- import { _ as CodemationFrontendAuthSnapshotFactory, g as FrontendAppConfigFactory, h as InternalAuthBootstrapFactory, m as PublicFrontendBootstrapFactory, y as ApiPaths } from "./AppContainerFactory-BRU02PTm.js";
7
+ import "./CredentialServices-D8BBZH1i.js";
8
+ import { _ as CodemationFrontendAuthSnapshotFactory, g as FrontendAppConfigFactory, h as InternalAuthBootstrapFactory, m as PublicFrontendBootstrapFactory, y as ApiPaths } from "./AppContainerFactory-BKaAUIi-.js";
9
9
  import "./AppConfigFactory-ByT1D8dM.js";
10
10
  import { t as CodemationPostgresPrismaClientFactory } from "./persistenceServer-DMvIOGW8.js";
11
- import { n as AppConfigLoader, r as CodemationServerGateway, t as CodemationPluginDiscovery } from "./server-ChTCEc6R.js";
11
+ import { n as AppConfigLoader, r as CodemationServerGateway, t as CodemationPluginDiscovery } from "./server-C-WZcsId.js";
12
12
 
13
13
  export { ApiPaths, AppConfigLoader, CodemationConsumerAppResolver, CodemationConsumerConfigLoader, CodemationFrontendAuthSnapshotFactory, CodemationFrontendAuthSnapshotJsonCodec, CodemationPluginDiscovery, CodemationPostgresPrismaClientFactory, CodemationServerGateway, FrontendAppConfigFactory, FrontendAppConfigJsonCodec, InternalAuthBootstrapFactory, InternalAuthBootstrapJsonCodec, PublicFrontendBootstrapFactory, PublicFrontendBootstrapJsonCodec, WorkflowDiscoveryPathSegmentsComputer, WorkflowModulePathFinder };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemation/host",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -112,9 +112,9 @@
112
112
  "tsx": "^4.21.0",
113
113
  "ws": "^8.19.0",
114
114
  "zxcvbn": "^4.4.2",
115
- "@codemation/core": "0.3.0",
116
- "@codemation/core-nodes": "0.0.25",
117
- "@codemation/eventbus-redis": "0.0.25"
115
+ "@codemation/core": "0.5.0",
116
+ "@codemation/core-nodes": "0.1.1",
117
+ "@codemation/eventbus-redis": "0.0.27"
118
118
  },
119
119
  "devDependencies": {
120
120
  "@playwright/test": "^1.58.2",
@@ -139,9 +139,10 @@
139
139
  "tsdown": "^0.15.5",
140
140
  "typescript": "^5.9.3",
141
141
  "vitest": "^4.0.18",
142
- "@codemation/core-nodes-gmail": "0.0.27"
142
+ "@codemation/core-nodes-gmail": "0.1.0"
143
143
  },
144
144
  "scripts": {
145
+ "changeset:verify": "pnpm --workspace-root run changeset:verify",
145
146
  "prisma:generate": "node ./scripts/generate-prisma-clients.mjs && node ./scripts/ensure-prisma-runtime-sourcemaps.mjs",
146
147
  "build": "tsdown src/index.ts src/client.ts src/credentials.ts src/consumer.ts src/nextServer.ts src/server.ts src/devServerSidecar.ts src/persistenceServer.ts --out-dir dist --sourcemap",
147
148
  "dev": "tsdown src/index.ts src/client.ts src/credentials.ts src/consumer.ts src/nextServer.ts src/server.ts src/devServerSidecar.ts src/persistenceServer.ts --out-dir dist --sourcemap --watch",
@@ -16,10 +16,14 @@ if (!fs.existsSync(preparedPath)) {
16
16
 
17
17
  const prepared = JSON.parse(fs.readFileSync(preparedPath, "utf8")) as CodemationPlaywrightPreparedEnvironment;
18
18
 
19
- const webServerEnv: NodeJS.ProcessEnv = {
20
- ...process.env,
21
- ...prepared.serverEnv,
22
- };
19
+ const webServerEnv: Record<string, string> = Object.fromEntries(
20
+ Object.entries({
21
+ ...process.env,
22
+ ...prepared.serverEnv,
23
+ }).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
24
+ );
25
+
26
+ const baseUrl = String(prepared.serverEnv.AUTH_URL ?? "http://localhost:3001");
23
27
 
24
28
  export default defineConfig({
25
29
  /**
@@ -49,7 +53,7 @@ export default defineConfig({
49
53
  ],
50
54
  use: {
51
55
  /** Align with app URL and AUTH_URL so Auth.js cookies are not split across localhost vs 127.0.0.1. */
52
- baseURL: "http://localhost:3001",
56
+ baseURL: baseUrl,
53
57
  trace: "retain-on-failure",
54
58
  video: "retain-on-failure",
55
59
  },
@@ -64,7 +68,7 @@ export default defineConfig({
64
68
  command: "pnpm run e2e:serve-web",
65
69
  cwd: repoRoot,
66
70
  env: webServerEnv,
67
- url: "http://localhost:3001",
71
+ url: baseUrl,
68
72
  // Always start a fresh server so DATABASE_URL/AUTH_SECRET from `.e2e-prepared.json` match the DB that was provisioned for this run (reuse can leave a stale server on port 3001).
69
73
  reuseExistingServer: false,
70
74
  timeout: 180_000,
@@ -12,6 +12,9 @@ export type WorkflowNodeDto = Readonly<{
12
12
  hasNodeErrorHandler?: boolean;
13
13
  /** When true, downstream nodes may run even when this node outputs zero items. */
14
14
  continueWhenEmptyOutput?: boolean;
15
+ /** Declared I/O ports from node config (unioned with ports inferred from edges on the canvas). */
16
+ declaredOutputPorts?: ReadonlyArray<string>;
17
+ declaredInputPorts?: ReadonlyArray<string>;
15
18
  }>;
16
19
 
17
20
  export type WorkflowEdgeDto = Readonly<{
@@ -1,4 +1,4 @@
1
- import type { AgentNodeConfig, NodeDefinition, WorkflowActivationPolicy, WorkflowDefinition } from "@codemation/core";
1
+ import type { NodeDefinition, WorkflowActivationPolicy, WorkflowDefinition } from "@codemation/core";
2
2
  import {
3
3
  AgentConfigInspector,
4
4
  AgentConnectionNodeCollector,
@@ -30,13 +30,14 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
30
30
  }
31
31
 
32
32
  mapSync(workflow: WorkflowDefinition): WorkflowDto {
33
+ const mapped = this.mapNodesAndEdges(workflow);
33
34
  return {
34
35
  id: workflow.id,
35
36
  name: workflow.name,
36
37
  active: this.workflowActivationPolicy.isActive(workflow.id),
37
38
  hasWorkflowErrorHandler: this.policyUi.workflowHasErrorHandler(workflow),
38
- nodes: this.toNodes(workflow),
39
- edges: this.toEdges(workflow),
39
+ nodes: mapped.nodes,
40
+ edges: mapped.edges,
40
41
  };
41
42
  }
42
43
 
@@ -61,19 +62,54 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
61
62
  return map;
62
63
  }
63
64
 
64
- private toNodes(workflow: WorkflowDefinition): ReadonlyArray<WorkflowNodeDto> {
65
+ private mapNodesAndEdges(
66
+ workflow: WorkflowDefinition,
67
+ ): Readonly<{ nodes: ReadonlyArray<WorkflowNodeDto>; edges: WorkflowDto["edges"] }> {
65
68
  const connectionChildMeta = this.buildConnectionChildMeta(workflow);
66
69
  const materializedConnectionNodeIds = new Set(connectionChildMeta.keys());
70
+ const nodesById = new Map(workflow.nodes.map((node) => [node.id, node] as const));
71
+ const agentConnectionDescriptors = this.buildAgentConnectionDescriptorIndex(workflow);
72
+ return {
73
+ nodes: this.toNodes({
74
+ workflow,
75
+ connectionChildMeta,
76
+ materializedConnectionNodeIds,
77
+ nodesById,
78
+ agentConnectionDescriptors,
79
+ }),
80
+ edges: this.toEdges({
81
+ workflow,
82
+ materializedConnectionNodeIds,
83
+ agentConnectionDescriptors,
84
+ }),
85
+ };
86
+ }
87
+
88
+ private toNodes(
89
+ args: Readonly<{
90
+ workflow: WorkflowDefinition;
91
+ connectionChildMeta: ReadonlyMap<string, Readonly<{ parentNodeId: string; connectionName: string }>>;
92
+ materializedConnectionNodeIds: ReadonlySet<string>;
93
+ nodesById: ReadonlyMap<string, WorkflowDefinition["nodes"][number]>;
94
+ agentConnectionDescriptors: Readonly<{
95
+ byAgentNodeId: ReadonlyMap<string, ReadonlyArray<AgentConnectionNodeDescriptor>>;
96
+ byChildNodeIdByAgentNodeId: ReadonlyMap<string, ReadonlyMap<string, AgentConnectionNodeDescriptor>>;
97
+ }>;
98
+ }>,
99
+ ): ReadonlyArray<WorkflowNodeDto> {
100
+ const workflow = args.workflow;
101
+ const connectionChildMeta = args.connectionChildMeta;
102
+ const materializedConnectionNodeIds = args.materializedConnectionNodeIds;
103
+ const nodesById = args.nodesById;
104
+ const agentConnectionDescriptors = args.agentConnectionDescriptors;
67
105
  const nodes: WorkflowNodeDto[] = [];
68
106
  for (const node of workflow.nodes) {
69
107
  const conn = connectionChildMeta.get(node.id);
70
108
  if (conn) {
71
- const parentNode = workflow.nodes.find((n) => n.id === conn.parentNodeId);
109
+ const parentNode = nodesById.get(conn.parentNodeId);
72
110
  let role: string = conn.connectionName === "llm" ? "languageModel" : "tool";
73
111
  if (parentNode && AgentConfigInspector.isAgentNodeConfig(parentNode.config)) {
74
- const descriptor = AgentConnectionNodeCollector.collect(conn.parentNodeId, parentNode.config).find(
75
- (d) => d.nodeId === node.id,
76
- );
112
+ const descriptor = agentConnectionDescriptors.byChildNodeIdByAgentNodeId.get(conn.parentNodeId)?.get(node.id);
77
113
  if (descriptor) {
78
114
  role = descriptor.role;
79
115
  }
@@ -87,6 +123,7 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
87
123
  icon: node.config?.icon,
88
124
  retryPolicySummary: this.policyUi.nodeRetrySummary(node.config),
89
125
  hasNodeErrorHandler: this.policyUi.nodeHasErrorHandler(node.config),
126
+ ...this.nodePortFieldsFromConfig(node.config),
90
127
  parentNodeId: conn.parentNodeId,
91
128
  });
92
129
  continue;
@@ -100,17 +137,32 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
100
137
  icon: node.config?.icon,
101
138
  retryPolicySummary: this.policyUi.nodeRetrySummary(node.config),
102
139
  hasNodeErrorHandler: this.policyUi.nodeHasErrorHandler(node.config),
140
+ ...this.nodePortFieldsFromConfig(node.config),
103
141
  });
104
142
  if (AgentConfigInspector.isAgentNodeConfig(node.config)) {
105
- this.appendVirtualConnectionNodes(node.id, node.config, materializedConnectionNodeIds, nodes);
143
+ this.appendVirtualConnectionNodes(
144
+ materializedConnectionNodeIds,
145
+ nodes,
146
+ agentConnectionDescriptors.byAgentNodeId.get(node.id) ?? [],
147
+ );
106
148
  }
107
149
  }
108
150
  return nodes;
109
151
  }
110
152
 
111
- private toEdges(workflow: WorkflowDefinition): WorkflowDto["edges"] {
112
- const connectionChildMeta = this.buildConnectionChildMeta(workflow);
113
- const materializedConnectionNodeIds = new Set(connectionChildMeta.keys());
153
+ private toEdges(
154
+ args: Readonly<{
155
+ workflow: WorkflowDefinition;
156
+ materializedConnectionNodeIds: ReadonlySet<string>;
157
+ agentConnectionDescriptors: Readonly<{
158
+ byAgentNodeId: ReadonlyMap<string, ReadonlyArray<AgentConnectionNodeDescriptor>>;
159
+ byChildNodeIdByAgentNodeId: ReadonlyMap<string, ReadonlyMap<string, AgentConnectionNodeDescriptor>>;
160
+ }>;
161
+ }>,
162
+ ): WorkflowDto["edges"] {
163
+ const workflow = args.workflow;
164
+ const materializedConnectionNodeIds = args.materializedConnectionNodeIds;
165
+ const agentConnectionDescriptors = args.agentConnectionDescriptors;
114
166
  const edges: WorkflowEdgeDto[] = [...workflow.edges];
115
167
  const edgeKeys = new Set(edges.map((edge) => this.edgeKey(edge.from.nodeId, edge.to.nodeId, edge.to.input)));
116
168
  this.appendMaterializedConnectionEdges(workflow, edgeKeys, edges);
@@ -118,7 +170,12 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
118
170
  if (!AgentConfigInspector.isAgentNodeConfig(node.config)) {
119
171
  continue;
120
172
  }
121
- this.appendVirtualConnectionEdges(node.id, node.config, materializedConnectionNodeIds, edgeKeys, edges);
173
+ this.appendVirtualConnectionEdges(
174
+ materializedConnectionNodeIds,
175
+ edgeKeys,
176
+ edges,
177
+ agentConnectionDescriptors.byAgentNodeId.get(node.id) ?? [],
178
+ );
122
179
  }
123
180
  return edges;
124
181
  }
@@ -143,13 +200,33 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
143
200
  }
144
201
  }
145
202
 
203
+ private buildAgentConnectionDescriptorIndex(workflow: WorkflowDefinition): Readonly<{
204
+ byAgentNodeId: ReadonlyMap<string, ReadonlyArray<AgentConnectionNodeDescriptor>>;
205
+ byChildNodeIdByAgentNodeId: ReadonlyMap<string, ReadonlyMap<string, AgentConnectionNodeDescriptor>>;
206
+ }> {
207
+ const byAgentNodeId = new Map<string, ReadonlyArray<AgentConnectionNodeDescriptor>>();
208
+ const byChildNodeIdByAgentNodeId = new Map<string, ReadonlyMap<string, AgentConnectionNodeDescriptor>>();
209
+ for (const node of workflow.nodes) {
210
+ if (!AgentConfigInspector.isAgentNodeConfig(node.config)) {
211
+ continue;
212
+ }
213
+ const descriptors = AgentConnectionNodeCollector.collect(node.id, node.config);
214
+ byAgentNodeId.set(node.id, descriptors);
215
+ const byChildId = new Map<string, AgentConnectionNodeDescriptor>();
216
+ for (const descriptor of descriptors) {
217
+ byChildId.set(descriptor.nodeId, descriptor);
218
+ }
219
+ byChildNodeIdByAgentNodeId.set(node.id, byChildId);
220
+ }
221
+ return { byAgentNodeId, byChildNodeIdByAgentNodeId };
222
+ }
223
+
146
224
  private appendVirtualConnectionNodes(
147
- rootAgentNodeId: string,
148
- agentConfig: AgentNodeConfig<any, any>,
149
225
  materializedConnectionNodeIds: ReadonlySet<string>,
150
226
  nodes: WorkflowNodeDto[],
227
+ descriptors: ReadonlyArray<AgentConnectionNodeDescriptor>,
151
228
  ): void {
152
- for (const connectionNode of AgentConnectionNodeCollector.collect(rootAgentNodeId, agentConfig)) {
229
+ for (const connectionNode of descriptors) {
153
230
  if (materializedConnectionNodeIds.has(connectionNode.nodeId)) {
154
231
  continue;
155
232
  }
@@ -158,13 +235,12 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
158
235
  }
159
236
 
160
237
  private appendVirtualConnectionEdges(
161
- rootAgentNodeId: string,
162
- agentConfig: AgentNodeConfig<any, any>,
163
238
  materializedConnectionNodeIds: ReadonlySet<string>,
164
239
  edgeKeys: Set<string>,
165
240
  edges: WorkflowEdgeDto[],
241
+ descriptors: ReadonlyArray<AgentConnectionNodeDescriptor>,
166
242
  ): void {
167
- for (const connectionNode of AgentConnectionNodeCollector.collect(rootAgentNodeId, agentConfig)) {
243
+ for (const connectionNode of descriptors) {
168
244
  if (materializedConnectionNodeIds.has(connectionNode.nodeId)) {
169
245
  continue;
170
246
  }
@@ -196,6 +272,28 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
196
272
  };
197
273
  }
198
274
 
275
+ /**
276
+ * Omit optional port fields when undefined so persisted snapshot DTOs (which never serialize
277
+ * undefined keys) stay aligned with live workflow mapping.
278
+ */
279
+ private nodePortFieldsFromConfig(
280
+ config: NodeDefinition["config"] | undefined,
281
+ ): Pick<WorkflowNodeDto, "continueWhenEmptyOutput" | "declaredOutputPorts" | "declaredInputPorts"> {
282
+ if (!config || typeof config !== "object") {
283
+ return {};
284
+ }
285
+ const c = config as {
286
+ continueWhenEmptyOutput?: boolean;
287
+ declaredOutputPorts?: readonly string[];
288
+ declaredInputPorts?: readonly string[];
289
+ };
290
+ return {
291
+ ...(c.continueWhenEmptyOutput !== undefined && { continueWhenEmptyOutput: c.continueWhenEmptyOutput }),
292
+ ...(c.declaredOutputPorts !== undefined && { declaredOutputPorts: c.declaredOutputPorts }),
293
+ ...(c.declaredInputPorts !== undefined && { declaredInputPorts: c.declaredInputPorts }),
294
+ };
295
+ }
296
+
199
297
  private nodeTypeName(node: NodeDefinition): string {
200
298
  const configToken = node.config?.type as Readonly<{ name?: unknown }> | undefined;
201
299
  if (typeof configToken?.name === "string" && configToken.name) {
@@ -63,6 +63,7 @@ import {
63
63
  GetRunStateQueryHandler,
64
64
  GetWorkflowDebuggerOverlayQueryHandler,
65
65
  GetWorkflowDetailQueryHandler,
66
+ GetWorkflowRunDetailQueryHandler,
66
67
  GetWorkflowOverlayBinaryAttachmentQueryHandler,
67
68
  GetWorkflowSummariesQueryHandler,
68
69
  ListWorkflowRunsQueryHandler,
@@ -194,6 +195,7 @@ export class AppContainerFactory {
194
195
  VerifyUserInviteQueryHandler,
195
196
  GetRunBinaryAttachmentQueryHandler,
196
197
  GetRunStateQueryHandler,
198
+ GetWorkflowRunDetailQueryHandler,
197
199
  GetWorkflowDebuggerOverlayQueryHandler,
198
200
  GetWorkflowDetailQueryHandler,
199
201
  GetWorkflowOverlayBinaryAttachmentQueryHandler,
@@ -23,6 +23,7 @@ import { ApplicationTokens } from "../../applicationTokens";
23
23
 
24
24
  import { CredentialFieldEnvOverlayService } from "./CredentialFieldEnvOverlayService";
25
25
  import { CredentialMaterialResolver } from "./CredentialMaterialResolver";
26
+ import { CredentialOAuth2ScopeResolver } from "./CredentialOAuth2ScopeResolver";
26
27
  import { CredentialSecretCipher } from "./CredentialSecretCipher";
27
28
  import type {
28
29
  CredentialInstanceRecord,
@@ -49,6 +50,8 @@ export class CredentialInstanceService {
49
50
  private readonly credentialFieldEnvOverlayService: CredentialFieldEnvOverlayService,
50
51
  @inject(CredentialMaterialResolver)
51
52
  private readonly credentialMaterialResolver: CredentialMaterialResolver,
53
+ @inject(CredentialOAuth2ScopeResolver)
54
+ private readonly credentialOAuth2ScopeResolver: CredentialOAuth2ScopeResolver,
52
55
  @inject(CoreTokens.CredentialSessionService)
53
56
  private readonly credentialSessionService: MutableCredentialSessionService,
54
57
  ) {}
@@ -373,10 +376,14 @@ export class CredentialInstanceService {
373
376
  "providerId" in credentialType.definition.auth ? credentialType.definition.auth.providerId : "custom";
374
377
  const material = await this.credentialStore.getOAuth2Material(instance.instanceId);
375
378
  if (!material) {
379
+ const requestedScopes = this.credentialOAuth2ScopeResolver.resolveRequestedScopes(
380
+ credentialType.definition.auth,
381
+ instance.publicConfig,
382
+ );
376
383
  return {
377
384
  status: "disconnected",
378
385
  providerId,
379
- scopes: [...credentialType.definition.auth.scopes],
386
+ scopes: [...requestedScopes],
380
387
  };
381
388
  }
382
389
  return {
@@ -0,0 +1,61 @@
1
+ import type { CredentialOAuth2AuthDefinition } from "@codemation/core";
2
+ import { injectable } from "@codemation/core";
3
+ import type { JsonRecord } from "./CredentialServices";
4
+
5
+ @injectable()
6
+ export class CredentialOAuth2ScopeResolver {
7
+ resolveRequestedScopes(auth: CredentialOAuth2AuthDefinition, publicConfig: JsonRecord): ReadonlyArray<string> {
8
+ const scopesFromPublicConfig = auth.scopesFromPublicConfig;
9
+ if (!scopesFromPublicConfig) {
10
+ return [...auth.scopes];
11
+ }
12
+ const preset = this.resolveString(publicConfig[scopesFromPublicConfig.presetFieldKey]);
13
+ if (!preset) {
14
+ return [...auth.scopes];
15
+ }
16
+ const presetScopes = scopesFromPublicConfig.presetScopes[preset];
17
+ if (presetScopes) {
18
+ return [...presetScopes];
19
+ }
20
+ const customPresetKey = scopesFromPublicConfig.customPresetKey ?? "custom";
21
+ if (preset !== customPresetKey) {
22
+ return [...auth.scopes];
23
+ }
24
+ const customScopes = this.resolveScopeList(
25
+ publicConfig[scopesFromPublicConfig.customScopesFieldKey ?? "customScopes"],
26
+ );
27
+ if (customScopes.length > 0) {
28
+ return customScopes;
29
+ }
30
+ return [...auth.scopes];
31
+ }
32
+
33
+ private resolveString(value: unknown): string | undefined {
34
+ if (typeof value !== "string") {
35
+ return undefined;
36
+ }
37
+ const normalized = value.trim();
38
+ return normalized.length > 0 ? normalized : undefined;
39
+ }
40
+
41
+ private resolveScopeList(value: unknown): ReadonlyArray<string> {
42
+ if (Array.isArray(value)) {
43
+ return this.dedupe(
44
+ value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter((entry) => entry.length > 0),
45
+ );
46
+ }
47
+ if (typeof value !== "string") {
48
+ return [];
49
+ }
50
+ return this.dedupe(
51
+ value
52
+ .split(/[\s,]+/)
53
+ .map((entry) => entry.trim())
54
+ .filter((entry) => entry.length > 0),
55
+ );
56
+ }
57
+
58
+ private dedupe(entries: ReadonlyArray<string>): ReadonlyArray<string> {
59
+ return [...new Set(entries)];
60
+ }
61
+ }
@@ -138,6 +138,7 @@ export { CredentialTypeRegistryImpl } from "./CredentialTypeRegistryImpl";
138
138
  export { CredentialBindingService } from "./CredentialBindingService";
139
139
  export { CredentialInstanceService } from "./CredentialInstanceService";
140
140
  export { CredentialMaterialResolver } from "./CredentialMaterialResolver";
141
+ export { CredentialOAuth2ScopeResolver } from "./CredentialOAuth2ScopeResolver";
141
142
  export { CredentialRuntimeMaterialService } from "./CredentialRuntimeMaterialService";
142
143
  export { CredentialFieldEnvOverlayService } from "./CredentialFieldEnvOverlayService";
143
144
  export { CredentialSecretCipher } from "./CredentialSecretCipher";
@@ -9,6 +9,7 @@ import {
9
9
  CredentialFieldEnvOverlayService,
10
10
  CredentialInstanceService,
11
11
  CredentialMaterialResolver,
12
+ CredentialOAuth2ScopeResolver,
12
13
  CredentialRuntimeMaterialService,
13
14
  CredentialSecretCipher,
14
15
  CredentialTypeRegistryImpl,
@@ -47,6 +48,8 @@ export class OAuth2ConnectService {
47
48
  private readonly credentialMaterialResolver: CredentialMaterialResolver,
48
49
  @inject(CredentialSecretCipher)
49
50
  private readonly credentialSecretCipher: CredentialSecretCipher,
51
+ @inject(CredentialOAuth2ScopeResolver)
52
+ private readonly credentialOAuth2ScopeResolver: CredentialOAuth2ScopeResolver,
50
53
  @inject(OAuth2ProviderRegistry)
51
54
  private readonly oauth2ProviderRegistry: OAuth2ProviderRegistry,
52
55
  @inject(ApplicationTokens.AppConfig)
@@ -63,6 +66,10 @@ export class OAuth2ConnectService {
63
66
  material: emptyMaterial,
64
67
  });
65
68
  const provider = this.oauth2ProviderRegistry.resolve(credentialType.definition, resolvedPublicConfig);
69
+ const requestedScopes = this.credentialOAuth2ScopeResolver.resolveRequestedScopes(
70
+ credentialType.definition.auth!,
71
+ resolvedPublicConfig,
72
+ );
66
73
  const redirectUri = this.getRedirectUri(requestOrigin);
67
74
  const state = this.createOpaqueValue();
68
75
  const codeVerifier = this.createOpaqueValue();
@@ -74,7 +81,7 @@ export class OAuth2ConnectService {
74
81
  instanceId,
75
82
  codeVerifier,
76
83
  providerId: provider.providerId,
77
- requestedScopes: credentialType.definition.auth!.scopes,
84
+ requestedScopes,
78
85
  createdAt: createdAt.toISOString(),
79
86
  expiresAt: expiresAt.toISOString(),
80
87
  });
@@ -85,7 +92,7 @@ export class OAuth2ConnectService {
85
92
  this.oauth2ProviderRegistry.resolveClientId(credentialType.definition.auth!, resolvedPublicConfig),
86
93
  );
87
94
  authorizeUrl.searchParams.set("redirect_uri", redirectUri);
88
- authorizeUrl.searchParams.set("scope", credentialType.definition.auth!.scopes.join(" "));
95
+ authorizeUrl.searchParams.set("scope", requestedScopes.join(" "));
89
96
  authorizeUrl.searchParams.set("state", state);
90
97
  authorizeUrl.searchParams.set("code_challenge", codeChallenge);
91
98
  authorizeUrl.searchParams.set("code_challenge_method", "S256");
@@ -121,7 +121,8 @@ export class CredentialHttpRouteHandler {
121
121
 
122
122
  async getWorkflowCredentialHealth(_: Request, params: ServerHttpRouteParams): Promise<Response> {
123
123
  try {
124
- return Response.json(await this.queryBus.execute(new GetWorkflowCredentialHealthQuery(params.workflowId!)));
124
+ const health = await this.queryBus.execute(new GetWorkflowCredentialHealthQuery(params.workflowId!));
125
+ return Response.json(health);
125
126
  } catch (error) {
126
127
  return ServerHttpErrorResponseFactory.fromUnknown(error);
127
128
  }