@kb-labs/workflow-entry 2.89.0 → 2.94.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/dist/commands/health.js +1 -1
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/list.js +1 -1
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/logs.js +1 -1
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/metrics.js +1 -1
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/run.js +1 -1
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/status.js +2 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/workflow-run.js +1 -1
- package/dist/commands/workflow-run.js.map +1 -1
- package/dist/rest/cron-list-handler.js.map +1 -1
- package/dist/rest/index.js.map +1 -1
- package/dist/rest/job-cancel-handler.js.map +1 -1
- package/dist/rest/job-detail-handler.js.map +1 -1
- package/dist/rest/job-logs-handler.js.map +1 -1
- package/dist/rest/job-steps-handler.js.map +1 -1
- package/dist/rest/jobs-list-handler.js.map +1 -1
- package/dist/rest/pending-approvals-handler.js.map +1 -1
- package/dist/rest/resolve-approval-handler.js.map +1 -1
- package/dist/rest/run-detail-handler.js.map +1 -1
- package/dist/rest/runs-list-handler.js.map +1 -1
- package/dist/rest/stats-handler.js.map +1 -1
- package/dist/rest/workflow-detail-handler.js.map +1 -1
- package/dist/rest/workflow-run-cancel-handler.js.map +1 -1
- package/dist/rest/workflow-run-handler.js.map +1 -1
- package/dist/rest/workflow-runs-handler.js.map +1 -1
- package/dist/rest/workflows-list-handler.js.map +1 -1
- package/dist/widgets/__federation_expose_RunDetail.js.map +1 -1
- package/dist/widgets/mf-manifest.json +5 -5
- package/dist/widgets/mf-stats.json +5 -5
- package/dist/widgets/remoteEntry.js +2 -2
- package/dist/widgets/remoteEntry.js.map +1 -1
- package/dist/ws/logs-channel.js.map +1 -1
- package/dist/ws/progress-channel.js.map +1 -1
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/job-steps-handler.ts"],"names":[],"mappings":";;;AAMA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACRA,IAAO,4BAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EAC2B;AAC3B,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,KAAA,CAAM,MAAA;AAExB,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,aAAA,EAAgB,kBAAA,CAAmB,KAAK,CAAC,CAAA,MAAA,CAAA;AAEjE,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,wCAAA,EAA2C,GAAG,CAAA,CAAE,CAAA;AAEzE,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,eAAe,CAAA;AAAA,QACjC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,2BAA2B,CAAA;AAAA,MAC7D;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,8CAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"job-steps-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type { WorkflowRunRequest } from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as any).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics\n */\n async getMetrics(): Promise<any> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<any[]> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ logs: any[] }>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<any[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ jobs?: any[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job: any) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<{\n crons: any[];\n }> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<any>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/job-steps-handler\n * REST handler for job steps and progress (proxy to daemon)\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { JobStepsResponse } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, unknown, { jobId: string }>\n ): Promise<JobStepsResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { jobId } = input.params!;\n\n const url = `${daemonUrl}/api/v1/jobs/${encodeURIComponent(jobId)}/steps`;\n\n ctx.platform.logger.info(`[job-steps-handler] Fetching steps from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Job not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: JobStepsResponse; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch job steps');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[job-steps-handler] Error fetching job steps',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/job-steps-handler.ts"],"names":[],"mappings":";;;AA6DA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;AC/DA,IAAO,4BAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EAC2B;AAC3B,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,KAAA,CAAM,MAAA;AAExB,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,aAAA,EAAgB,kBAAA,CAAmB,KAAK,CAAC,CAAA,MAAA,CAAA;AAEjE,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,wCAAA,EAA2C,GAAG,CAAA,CAAE,CAAA;AAEzE,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,eAAe,CAAA;AAAA,QACjC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,2BAA2B,CAAA;AAAA,MAC7D;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,8CAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"job-steps-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/job-steps-handler\n * REST handler for job steps and progress (proxy to daemon)\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { JobStepsResponse } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, unknown, { jobId: string }>\n ): Promise<JobStepsResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { jobId } = input.params!;\n\n const url = `${daemonUrl}/api/v1/jobs/${encodeURIComponent(jobId)}/steps`;\n\n ctx.platform.logger.info(`[job-steps-handler] Fetching steps from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Job not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: JobStepsResponse; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch job steps');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[job-steps-handler] Error fetching job steps',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/jobs-list-handler.ts"],"names":[],"mappings":";;;AAMA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACoBA,IAAO,4BAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EAC0B;AAC1B,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,MAAM,MAAA,EAAQ,KAAA,EAAO,QAAO,GAAI,KAAA,CAAM,SAAS,EAAC;AAGxD,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,IAAA,IAAI,IAAA,EAAM;AAAC,MAAA,MAAA,CAAO,GAAA,CAAI,QAAQ,IAAI,CAAA;AAAA,IAAE;AACpC,IAAA,IAAI,MAAA,EAAQ;AAAC,MAAA,MAAA,CAAO,GAAA,CAAI,UAAU,MAAM,CAAA;AAAA,IAAE;AAC1C,IAAA,IAAI,KAAA,EAAO;AAAC,MAAA,MAAA,CAAO,GAAA,CAAI,SAAS,KAAK,CAAA;AAAA,IAAE;AACvC,IAAA,IAAI,MAAA,EAAQ;AAAC,MAAA,MAAA,CAAO,GAAA,CAAI,UAAU,MAAM,CAAA;AAAA,IAAE;AAE1C,IAAA,MAAM,WAAA,GAAc,OAAO,QAAA,EAAS;AACpC,IAAA,MAAM,GAAA,GAAM,GAAG,SAAS,CAAA,YAAA,EAAe,cAAc,CAAA,CAAA,EAAI,WAAW,KAAK,EAAE,CAAA,CAAA;AAE3E,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,uCAAA,EAA0C,GAAG,CAAA,CAAE,CAAA;AAExE,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,MAAA,EAAQ,KAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,cAAA,EAAgB;AAAA;AAClB,OACD,CAAA;AAED,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK,CAAE,MAAM,OAAO,EAAE,KAAA,EAAO,eAAA,EAAgB,CAAE,CAAA;AAChF,QAAA,GAAA,CAAI,QAAA,CAAS,OAAO,KAAA,CAAM,CAAA,0CAAA,EAA6C,KAAK,SAAA,CAAU,SAAS,CAAC,CAAA,CAAE,CAAA;AAClG,QAAA,MAAM,IAAI,KAAA,CAAM,SAAA,CAAU,KAAA,IAAS,2BAA2B,CAAA;AAAA,MAChE;AAEA,MAAA,MAAM,OAAA,GAAW,MAAM,QAAA,CAAS,IAAA,EAAK;AACrC,MAAA,IAAI,CAAC,OAAA,CAAQ,EAAA,IAAM,CAAC,QAAQ,IAAA,EAAM;AAChC,QAAA,MAAM,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAA,IAAS,2BAA2B,CAAA;AAAA,MAC9D;AACA,MAAA,MAAM,OAAO,OAAA,CAAQ,IAAA;AACrB,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,IAAA,CAAK,+BAA+B,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA,KAAA,CAAO,CAAA;AAC/E,MAAA,OAAO,IAAA;AAAA,IACT,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,8CAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"jobs-list-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type { WorkflowRunRequest } from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as any).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics\n */\n async getMetrics(): Promise<any> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<any[]> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ logs: any[] }>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<any[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ jobs?: any[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job: any) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<{\n crons: any[];\n }> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<any>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: GET /workflows/jobs\n * Proxies to workflow daemon GET /api/v1/jobs\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\ninterface JobsListQuery {\n type?: string;\n status?: string;\n limit?: string;\n offset?: string;\n}\n\ninterface JobStatusInfo {\n id: string;\n type: string;\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';\n tenantId?: string;\n createdAt?: string;\n startedAt?: string;\n finishedAt?: string;\n result?: unknown;\n error?: string;\n}\n\ninterface JobListResponse {\n jobs: JobStatusInfo[];\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<JobsListQuery>\n ): Promise<JobListResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { type, status, limit, offset } = input.query || {};\n\n // Build query string\n const params = new URLSearchParams();\n if (type) {params.set('type', type);}\n if (status) {params.set('status', status);}\n if (limit) {params.set('limit', limit);}\n if (offset) {params.set('offset', offset);}\n\n const queryString = params.toString();\n const url = `${daemonUrl}/api/v1/jobs${queryString ? `?${queryString}` : ''}`;\n\n ctx.platform.logger.info(`[jobs-list-handler] Fetching jobs from ${url}`);\n\n try {\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({ error: 'Unknown error' })) as { error?: string };\n ctx.platform.logger.error(`[jobs-list-handler] Failed to fetch jobs: ${JSON.stringify(errorData)}`);\n throw new Error(errorData.error || 'Failed to fetch jobs list');\n }\n\n const payload = (await response.json()) as ApiEnvelope<JobListResponse>;\n if (!payload.ok || !payload.data) {\n throw new Error(payload.error || 'Failed to fetch jobs list');\n }\n const data = payload.data;\n ctx.platform.logger.info(`[jobs-list-handler] Fetched ${data.jobs.length} jobs`);\n return data;\n } catch (error) {\n ctx.platform.logger.error(\n '[jobs-list-handler] Error fetching jobs list',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/jobs-list-handler.ts"],"names":[],"mappings":";;;AA6DA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACnCA,IAAO,4BAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EAC0B;AAC1B,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,MAAM,MAAA,EAAQ,KAAA,EAAO,QAAO,GAAI,KAAA,CAAM,SAAS,EAAC;AAGxD,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,IAAA,IAAI,IAAA,EAAM;AAAC,MAAA,MAAA,CAAO,GAAA,CAAI,QAAQ,IAAI,CAAA;AAAA,IAAE;AACpC,IAAA,IAAI,MAAA,EAAQ;AAAC,MAAA,MAAA,CAAO,GAAA,CAAI,UAAU,MAAM,CAAA;AAAA,IAAE;AAC1C,IAAA,IAAI,KAAA,EAAO;AAAC,MAAA,MAAA,CAAO,GAAA,CAAI,SAAS,KAAK,CAAA;AAAA,IAAE;AACvC,IAAA,IAAI,MAAA,EAAQ;AAAC,MAAA,MAAA,CAAO,GAAA,CAAI,UAAU,MAAM,CAAA;AAAA,IAAE;AAE1C,IAAA,MAAM,WAAA,GAAc,OAAO,QAAA,EAAS;AACpC,IAAA,MAAM,GAAA,GAAM,GAAG,SAAS,CAAA,YAAA,EAAe,cAAc,CAAA,CAAA,EAAI,WAAW,KAAK,EAAE,CAAA,CAAA;AAE3E,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,uCAAA,EAA0C,GAAG,CAAA,CAAE,CAAA;AAExE,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,MAAA,EAAQ,KAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,cAAA,EAAgB;AAAA;AAClB,OACD,CAAA;AAED,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK,CAAE,MAAM,OAAO,EAAE,KAAA,EAAO,eAAA,EAAgB,CAAE,CAAA;AAChF,QAAA,GAAA,CAAI,QAAA,CAAS,OAAO,KAAA,CAAM,CAAA,0CAAA,EAA6C,KAAK,SAAA,CAAU,SAAS,CAAC,CAAA,CAAE,CAAA;AAClG,QAAA,MAAM,IAAI,KAAA,CAAM,SAAA,CAAU,KAAA,IAAS,2BAA2B,CAAA;AAAA,MAChE;AAEA,MAAA,MAAM,OAAA,GAAW,MAAM,QAAA,CAAS,IAAA,EAAK;AACrC,MAAA,IAAI,CAAC,OAAA,CAAQ,EAAA,IAAM,CAAC,QAAQ,IAAA,EAAM;AAChC,QAAA,MAAM,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAA,IAAS,2BAA2B,CAAA;AAAA,MAC9D;AACA,MAAA,MAAM,OAAO,OAAA,CAAQ,IAAA;AACrB,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,IAAA,CAAK,+BAA+B,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA,KAAA,CAAO,CAAA;AAC/E,MAAA,OAAO,IAAA;AAAA,IACT,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,8CAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"jobs-list-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: GET /workflows/jobs\n * Proxies to workflow daemon GET /api/v1/jobs\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\ninterface JobsListQuery {\n type?: string;\n status?: string;\n limit?: string;\n offset?: string;\n}\n\ninterface JobStatusInfo {\n id: string;\n type: string;\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';\n tenantId?: string;\n createdAt?: string;\n startedAt?: string;\n finishedAt?: string;\n result?: unknown;\n error?: string;\n}\n\ninterface JobListResponse {\n jobs: JobStatusInfo[];\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<JobsListQuery>\n ): Promise<JobListResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { type, status, limit, offset } = input.query || {};\n\n // Build query string\n const params = new URLSearchParams();\n if (type) {params.set('type', type);}\n if (status) {params.set('status', status);}\n if (limit) {params.set('limit', limit);}\n if (offset) {params.set('offset', offset);}\n\n const queryString = params.toString();\n const url = `${daemonUrl}/api/v1/jobs${queryString ? `?${queryString}` : ''}`;\n\n ctx.platform.logger.info(`[jobs-list-handler] Fetching jobs from ${url}`);\n\n try {\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({ error: 'Unknown error' })) as { error?: string };\n ctx.platform.logger.error(`[jobs-list-handler] Failed to fetch jobs: ${JSON.stringify(errorData)}`);\n throw new Error(errorData.error || 'Failed to fetch jobs list');\n }\n\n const payload = (await response.json()) as ApiEnvelope<JobListResponse>;\n if (!payload.ok || !payload.data) {\n throw new Error(payload.error || 'Failed to fetch jobs list');\n }\n const data = payload.data;\n ctx.platform.logger.info(`[jobs-list-handler] Fetched ${data.jobs.length} jobs`);\n return data;\n } catch (error) {\n ctx.platform.logger.error(\n '[jobs-list-handler] Error fetching jobs list',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/pending-approvals-handler.ts"],"names":[],"mappings":";;;AAMA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACCA,IAAO,oCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACkB;AAClB,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,KAAA,CAAM,MAAA;AAExB,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAAA,IAC3C;AAEA,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,aAAA,EAAgB,kBAAA,CAAmB,KAAK,CAAC,CAAA,kBAAA,CAAA;AAEjE,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,+DAAA,EAAkE,KAAK,CAAA,CAAE,CAAA;AAElG,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,eAAe,CAAA;AAAA,QACjC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,mCAAmC,CAAA;AAAA,MACrE;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,8DAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"pending-approvals-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type { WorkflowRunRequest } from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as any).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics\n */\n async getMetrics(): Promise<any> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<any[]> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ logs: any[] }>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<any[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ jobs?: any[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job: any) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<{\n crons: any[];\n }> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<any>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: GET /runs/:runId/pending-approvals\n * Proxies to workflow daemon GET /api/v1/runs/:runId/pending-approvals\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\ninterface PendingApprovalsParams {\n runId: string;\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, unknown, PendingApprovalsParams>\n ): Promise<unknown> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { runId } = input.params!;\n\n if (!runId) {\n throw new Error('Missing runId parameter');\n }\n\n const url = `${daemonUrl}/api/v1/runs/${encodeURIComponent(runId)}/pending-approvals`;\n\n ctx.platform.logger.info(`[pending-approvals-handler] Fetching pending approvals for run ${runId}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Run not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as ApiEnvelope<unknown>;\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch pending approvals');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[pending-approvals-handler] Error fetching pending approvals',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/pending-approvals-handler.ts"],"names":[],"mappings":";;;AA6DA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACtDA,IAAO,oCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACkB;AAClB,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,KAAA,CAAM,MAAA;AAExB,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAAA,IAC3C;AAEA,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,aAAA,EAAgB,kBAAA,CAAmB,KAAK,CAAC,CAAA,kBAAA,CAAA;AAEjE,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,+DAAA,EAAkE,KAAK,CAAA,CAAE,CAAA;AAElG,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,eAAe,CAAA;AAAA,QACjC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,mCAAmC,CAAA;AAAA,MACrE;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,8DAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"pending-approvals-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: GET /runs/:runId/pending-approvals\n * Proxies to workflow daemon GET /api/v1/runs/:runId/pending-approvals\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\ninterface PendingApprovalsParams {\n runId: string;\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, unknown, PendingApprovalsParams>\n ): Promise<unknown> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { runId } = input.params!;\n\n if (!runId) {\n throw new Error('Missing runId parameter');\n }\n\n const url = `${daemonUrl}/api/v1/runs/${encodeURIComponent(runId)}/pending-approvals`;\n\n ctx.platform.logger.info(`[pending-approvals-handler] Fetching pending approvals for run ${runId}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Run not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as ApiEnvelope<unknown>;\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch pending approvals');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[pending-approvals-handler] Error fetching pending approvals',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/resolve-approval-handler.ts"],"names":[],"mappings":";;;AAMA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACSA,IAAO,mCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACkB;AAClB,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,KAAA,CAAM,MAAA;AACxB,IAAA,MAAM,OAAO,KAAA,CAAM,IAAA;AAEnB,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAAA,IAC3C;AAEA,IAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,IAAA,CAAK,KAAA,IAAS,CAAC,IAAA,CAAK,MAAA,IAAU,CAAC,IAAA,CAAK,MAAA,EAAQ;AACxD,MAAA,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAAA,IAClE;AAEA,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,aAAA,EAAgB,kBAAA,CAAmB,KAAK,CAAC,CAAA,QAAA,CAAA;AAEjE,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,sDAAA,EAAyD,KAAK,CAAA,OAAA,EAAU,IAAA,CAAK,MAAM,CAAA,EAAA,EAAK,IAAA,CAAK,MAAM,CAAA,CAAE,CAAA;AAE9H,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI;AAAA,OAC1B,CAAA;AAED,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,6BAA6B,CAAA;AAAA,QAC/C;AACA,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,SAAA,GAAa,MAAM,QAAA,CAAS,IAAA,EAAK;AACvC,UAAA,MAAM,IAAI,KAAA,CAAM,SAAA,CAAU,KAAA,IAAS,kCAAkC,CAAA;AAAA,QACvE;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,4BAA4B,CAAA;AAAA,MAC9D;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,qDAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"resolve-approval-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type { WorkflowRunRequest } from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as any).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics\n */\n async getMetrics(): Promise<any> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<any[]> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ logs: any[] }>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<any[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ jobs?: any[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job: any) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<{\n crons: any[];\n }> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<any>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: POST /runs/:runId/approve\n * Proxies to workflow daemon POST /api/v1/runs/:runId/approve\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\ninterface ResolveApprovalParams {\n runId: string;\n}\n\ninterface ResolveApprovalBody {\n jobId: string;\n stepId: string;\n action: 'approve' | 'reject';\n comment?: string;\n data?: Record<string, unknown>;\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, ResolveApprovalBody, ResolveApprovalParams>\n ): Promise<unknown> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { runId } = input.params!;\n const body = input.body;\n\n if (!runId) {\n throw new Error('Missing runId parameter');\n }\n\n if (!body || !body.jobId || !body.stepId || !body.action) {\n throw new Error('Missing required fields: jobId, stepId, action');\n }\n\n const url = `${daemonUrl}/api/v1/runs/${encodeURIComponent(runId)}/approve`;\n\n ctx.platform.logger.info(`[resolve-approval-handler] Resolving approval for run ${runId}, step ${body.stepId}: ${body.action}`);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Run, job, or step not found');\n }\n if (response.status === 409) {\n const errResult = (await response.json()) as ApiEnvelope<unknown>;\n throw new Error(errResult.error || 'Step is not waiting for approval');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as ApiEnvelope<unknown>;\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to resolve approval');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[resolve-approval-handler] Error resolving approval',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/resolve-approval-handler.ts"],"names":[],"mappings":";;;AA6DA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;AC9CA,IAAO,mCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACkB;AAClB,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,KAAA,CAAM,MAAA;AACxB,IAAA,MAAM,OAAO,KAAA,CAAM,IAAA;AAEnB,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAAA,IAC3C;AAEA,IAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,IAAA,CAAK,KAAA,IAAS,CAAC,IAAA,CAAK,MAAA,IAAU,CAAC,IAAA,CAAK,MAAA,EAAQ;AACxD,MAAA,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAAA,IAClE;AAEA,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,aAAA,EAAgB,kBAAA,CAAmB,KAAK,CAAC,CAAA,QAAA,CAAA;AAEjE,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,sDAAA,EAAyD,KAAK,CAAA,OAAA,EAAU,IAAA,CAAK,MAAM,CAAA,EAAA,EAAK,IAAA,CAAK,MAAM,CAAA,CAAE,CAAA;AAE9H,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI;AAAA,OAC1B,CAAA;AAED,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,6BAA6B,CAAA;AAAA,QAC/C;AACA,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,SAAA,GAAa,MAAM,QAAA,CAAS,IAAA,EAAK;AACvC,UAAA,MAAM,IAAI,KAAA,CAAM,SAAA,CAAU,KAAA,IAAS,kCAAkC,CAAA;AAAA,QACvE;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,4BAA4B,CAAA;AAAA,MAC9D;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,qDAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"resolve-approval-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: POST /runs/:runId/approve\n * Proxies to workflow daemon POST /api/v1/runs/:runId/approve\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\ninterface ResolveApprovalParams {\n runId: string;\n}\n\ninterface ResolveApprovalBody {\n jobId: string;\n stepId: string;\n action: 'approve' | 'reject';\n comment?: string;\n data?: Record<string, unknown>;\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, ResolveApprovalBody, ResolveApprovalParams>\n ): Promise<unknown> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { runId } = input.params!;\n const body = input.body;\n\n if (!runId) {\n throw new Error('Missing runId parameter');\n }\n\n if (!body || !body.jobId || !body.stepId || !body.action) {\n throw new Error('Missing required fields: jobId, stepId, action');\n }\n\n const url = `${daemonUrl}/api/v1/runs/${encodeURIComponent(runId)}/approve`;\n\n ctx.platform.logger.info(`[resolve-approval-handler] Resolving approval for run ${runId}, step ${body.stepId}: ${body.action}`);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Run, job, or step not found');\n }\n if (response.status === 409) {\n const errResult = (await response.json()) as ApiEnvelope<unknown>;\n throw new Error(errResult.error || 'Step is not waiting for approval');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as ApiEnvelope<unknown>;\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to resolve approval');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[resolve-approval-handler] Error resolving approval',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/run-detail-handler.ts"],"names":[],"mappings":";;;AAMA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACEA,IAAO,6BAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACkB;AAClB,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,KAAA,CAAM,MAAA;AAExB,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAAA,IAC3C;AAEA,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,aAAA,EAAgB,kBAAA,CAAmB,KAAK,CAAC,CAAA,CAAA;AAEjE,IAAA,GAAA,CAAI,SAAS,MAAA,CAAO,IAAA,CAAK,qCAAqC,KAAK,CAAA,MAAA,EAAS,GAAG,CAAA,CAAE,CAAA;AAEjF,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,eAAe,CAAA;AAAA,QACjC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,qBAAqB,CAAA;AAAA,MACvD;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,yCAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"run-detail-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type { WorkflowRunRequest } from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as any).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics\n */\n async getMetrics(): Promise<any> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<any[]> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ logs: any[] }>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<any[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ jobs?: any[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job: any) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<{\n crons: any[];\n }> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<any>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: GET /runs/:runId\n * Proxies to workflow daemon GET /api/v1/runs/:runId\n * Returns full workflow run data including jobs and steps.\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\ninterface RunDetailParams {\n runId: string;\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, unknown, RunDetailParams>\n ): Promise<unknown> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { runId } = input.params!;\n\n if (!runId) {\n throw new Error('Missing runId parameter');\n }\n\n const url = `${daemonUrl}/api/v1/runs/${encodeURIComponent(runId)}`;\n\n ctx.platform.logger.info(`[run-detail-handler] Fetching run ${runId} from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Run not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as ApiEnvelope<unknown>;\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch run');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[run-detail-handler] Error fetching run',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/run-detail-handler.ts"],"names":[],"mappings":";;;AA6DA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACrDA,IAAO,6BAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACkB;AAClB,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,KAAA,CAAM,MAAA;AAExB,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAAA,IAC3C;AAEA,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,aAAA,EAAgB,kBAAA,CAAmB,KAAK,CAAC,CAAA,CAAA;AAEjE,IAAA,GAAA,CAAI,SAAS,MAAA,CAAO,IAAA,CAAK,qCAAqC,KAAK,CAAA,MAAA,EAAS,GAAG,CAAA,CAAE,CAAA;AAEjF,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,eAAe,CAAA;AAAA,QACjC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,qBAAqB,CAAA;AAAA,MACvD;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,yCAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"run-detail-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: GET /runs/:runId\n * Proxies to workflow daemon GET /api/v1/runs/:runId\n * Returns full workflow run data including jobs and steps.\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\ninterface RunDetailParams {\n runId: string;\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, unknown, RunDetailParams>\n ): Promise<unknown> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { runId } = input.params!;\n\n if (!runId) {\n throw new Error('Missing runId parameter');\n }\n\n const url = `${daemonUrl}/api/v1/runs/${encodeURIComponent(runId)}`;\n\n ctx.platform.logger.info(`[run-detail-handler] Fetching run ${runId} from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Run not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as ApiEnvelope<unknown>;\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch run');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[run-detail-handler] Error fetching run',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/runs-list-handler.ts"],"names":[],"mappings":";;;AAMA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACIA,IAAO,4BAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACkB;AAClB,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,MAAA,EAAQ,KAAA,EAAO,QAAO,GAAI,KAAA,CAAM,SAAS,EAAC;AAElD,IAAA,MAAM,cAAA,GAAiB,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,KAAK,GAAA,CAAI,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA,IAAK,EAAA,EAAI,CAAC,CAAA,EAAG,GAAI,CAAA,GAAI,EAAA;AACxF,IAAA,MAAM,eAAA,GAAkB,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,QAAA,CAAS,QAAQ,EAAE,CAAA,IAAK,CAAA,EAAG,CAAC,CAAA,GAAI,CAAA;AAC1E,IAAA,MAAM,aAAA,GAAgB,CAAC,QAAA,EAAU,SAAA,EAAW,WAAW,QAAA,EAAU,WAAA,EAAa,OAAO,SAAS,CAAA;AAC9F,IAAA,MAAM,kBAAkB,MAAA,IAAU,aAAA,CAAc,QAAA,CAAS,MAAM,IAAI,MAAA,GAAS,MAAA;AAE5E,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,IAAA,MAAA,CAAO,MAAA,CAAO,OAAA,EAAS,MAAA,CAAO,cAAc,CAAC,CAAA;AAC7C,IAAA,MAAA,CAAO,MAAA,CAAO,QAAA,EAAU,MAAA,CAAO,eAAe,CAAC,CAAA;AAC/C,IAAA,IAAI,eAAA,EAAiB;AACnB,MAAA,MAAA,CAAO,MAAA,CAAO,UAAU,eAAe,CAAA;AAAA,IACzC;AAEA,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,SAAS,CAAA,aAAA,EAAgB,MAAM,CAAA,CAAA;AAE9C,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,uCAAA,EAA0C,GAAG,CAAA,CAAE,CAAA;AAExE,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,sBAAsB,CAAA;AAAA,MACxD;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,yCAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"runs-list-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type { WorkflowRunRequest } from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as any).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics\n */\n async getMetrics(): Promise<any> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<any[]> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ logs: any[] }>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<any[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ jobs?: any[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job: any) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<{\n crons: any[];\n }> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<any>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: GET /runs\n * Proxies to workflow daemon GET /api/v1/runs\n * Lists all workflow runs across all workflows.\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\ninterface RunsListQuery {\n status?: string;\n limit?: string;\n offset?: string;\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<RunsListQuery>\n ): Promise<unknown> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { status, limit, offset } = input.query || {};\n\n const validatedLimit = limit ? Math.min(Math.max(parseInt(limit, 10) || 50, 1), 1000) : 50;\n const validatedOffset = offset ? Math.max(parseInt(offset, 10) || 0, 0) : 0;\n const validStatuses = ['queued', 'running', 'success', 'failed', 'cancelled', 'dlq', 'skipped'];\n const validatedStatus = status && validStatuses.includes(status) ? status : undefined;\n\n const params = new URLSearchParams();\n params.append('limit', String(validatedLimit));\n params.append('offset', String(validatedOffset));\n if (validatedStatus) {\n params.append('status', validatedStatus);\n }\n\n const url = `${daemonUrl}/api/v1/runs?${params}`;\n\n ctx.platform.logger.info(`[runs-list-handler] Fetching runs from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as ApiEnvelope<unknown>;\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch runs');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[runs-list-handler] Error fetching runs',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/runs-list-handler.ts"],"names":[],"mappings":";;;AA6DA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACnDA,IAAO,4BAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACkB;AAClB,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,MAAA,EAAQ,KAAA,EAAO,QAAO,GAAI,KAAA,CAAM,SAAS,EAAC;AAElD,IAAA,MAAM,cAAA,GAAiB,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,KAAK,GAAA,CAAI,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA,IAAK,EAAA,EAAI,CAAC,CAAA,EAAG,GAAI,CAAA,GAAI,EAAA;AACxF,IAAA,MAAM,eAAA,GAAkB,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,QAAA,CAAS,QAAQ,EAAE,CAAA,IAAK,CAAA,EAAG,CAAC,CAAA,GAAI,CAAA;AAC1E,IAAA,MAAM,aAAA,GAAgB,CAAC,QAAA,EAAU,SAAA,EAAW,WAAW,QAAA,EAAU,WAAA,EAAa,OAAO,SAAS,CAAA;AAC9F,IAAA,MAAM,kBAAkB,MAAA,IAAU,aAAA,CAAc,QAAA,CAAS,MAAM,IAAI,MAAA,GAAS,MAAA;AAE5E,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,IAAA,MAAA,CAAO,MAAA,CAAO,OAAA,EAAS,MAAA,CAAO,cAAc,CAAC,CAAA;AAC7C,IAAA,MAAA,CAAO,MAAA,CAAO,QAAA,EAAU,MAAA,CAAO,eAAe,CAAC,CAAA;AAC/C,IAAA,IAAI,eAAA,EAAiB;AACnB,MAAA,MAAA,CAAO,MAAA,CAAO,UAAU,eAAe,CAAA;AAAA,IACzC;AAEA,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,SAAS,CAAA,aAAA,EAAgB,MAAM,CAAA,CAAA;AAE9C,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,uCAAA,EAA0C,GAAG,CAAA,CAAE,CAAA;AAExE,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,sBAAsB,CAAA;AAAA,MACxD;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,yCAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"runs-list-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: GET /runs\n * Proxies to workflow daemon GET /api/v1/runs\n * Lists all workflow runs across all workflows.\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client.js';\n\ninterface RunsListQuery {\n status?: string;\n limit?: string;\n offset?: string;\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<RunsListQuery>\n ): Promise<unknown> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { status, limit, offset } = input.query || {};\n\n const validatedLimit = limit ? Math.min(Math.max(parseInt(limit, 10) || 50, 1), 1000) : 50;\n const validatedOffset = offset ? Math.max(parseInt(offset, 10) || 0, 0) : 0;\n const validStatuses = ['queued', 'running', 'success', 'failed', 'cancelled', 'dlq', 'skipped'];\n const validatedStatus = status && validStatuses.includes(status) ? status : undefined;\n\n const params = new URLSearchParams();\n params.append('limit', String(validatedLimit));\n params.append('offset', String(validatedOffset));\n if (validatedStatus) {\n params.append('status', validatedStatus);\n }\n\n const url = `${daemonUrl}/api/v1/runs?${params}`;\n\n ctx.platform.logger.info(`[runs-list-handler] Fetching runs from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as ApiEnvelope<unknown>;\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch runs');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[runs-list-handler] Error fetching runs',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/stats-handler.ts"],"names":[],"mappings":";;;AAMA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACRA,IAAO,wBAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CAAQ,GAAA,EAAsB,MAAA,EAA6D;AAC/F,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,GAAA,GAAM,GAAG,SAAS,CAAA,aAAA,CAAA;AAExB,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,oCAAA,EAAuC,GAAG,CAAA,CAAE,CAAA;AAErE,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,uBAAuB,CAAA;AAAA,MACzD;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,sCAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"stats-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type { WorkflowRunRequest } from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as any).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics\n */\n async getMetrics(): Promise<any> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<any[]> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ logs: any[] }>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<any[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ jobs?: any[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job: any) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<{\n crons: any[];\n }> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<any>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/stats-handler\n * REST handler for dashboard statistics (proxy to daemon)\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { DashboardStatsResponse } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\nexport default defineHandler({\n async execute(ctx: PluginContextV3, _input: RestInput<unknown>): Promise<DashboardStatsResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const url = `${daemonUrl}/api/v1/stats`;\n\n ctx.platform.logger.info(`[stats-handler] Fetching stats from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: DashboardStatsResponse; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch stats');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[stats-handler] Error fetching stats',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/stats-handler.ts"],"names":[],"mappings":";;;AA6DA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;AC/DA,IAAO,wBAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CAAQ,GAAA,EAAsB,MAAA,EAA6D;AAC/F,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,GAAA,GAAM,GAAG,SAAS,CAAA,aAAA,CAAA;AAExB,IAAA,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,CAAA,oCAAA,EAAuC,GAAG,CAAA,CAAE,CAAA;AAErE,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,uBAAuB,CAAA;AAAA,MACzD;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,sCAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"stats-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/stats-handler\n * REST handler for dashboard statistics (proxy to daemon)\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { DashboardStatsResponse } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\nexport default defineHandler({\n async execute(ctx: PluginContextV3, _input: RestInput<unknown>): Promise<DashboardStatsResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const url = `${daemonUrl}/api/v1/stats`;\n\n ctx.platform.logger.info(`[stats-handler] Fetching stats from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: DashboardStatsResponse; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch stats');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[stats-handler] Error fetching stats',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/workflow-detail-handler.ts"],"names":[],"mappings":";;;AAMA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACJA,IAAO,kCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACuB;AACvB,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,EAAA,EAAG,GAAI,KAAA,CAAM,MAAA;AAErB,IAAA,IAAI,CAAC,EAAA,EAAI;AACP,MAAA,MAAM,IAAI,MAAM,sBAAsB,CAAA;AAAA,IACxC;AAEA,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,kBAAA,EAAqB,kBAAA,CAAmB,EAAE,CAAC,CAAA,CAAA;AACnE,IAAA,GAAA,CAAI,SAAS,MAAA,CAAO,IAAA,CAAK,+CAA+C,EAAE,CAAA,MAAA,EAAS,GAAG,CAAA,CAAE,CAAA;AAExF,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,oBAAoB,CAAA;AAAA,QACtC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,0BAA0B,CAAA;AAAA,MAC5D;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,0DAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"workflow-detail-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type { WorkflowRunRequest } from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as any).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics\n */\n async getMetrics(): Promise<any> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<any[]> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ logs: any[] }>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<any[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ jobs?: any[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job: any) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<{\n crons: any[];\n }> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<any>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/workflow-detail-handler\n * REST handler for getting workflow definition details\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { WorkflowInfo } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\ninterface WorkflowDetailParams {\n id: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, unknown, WorkflowDetailParams>\n ): Promise<WorkflowInfo> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { id } = input.params!;\n\n if (!id) {\n throw new Error('Missing id parameter');\n }\n\n const url = `${daemonUrl}/api/v1/workflows/${encodeURIComponent(id)}`;\n ctx.platform.logger.info(`[workflow-detail-handler] Fetching workflow ${id} from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Workflow not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: WorkflowInfo; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch workflow');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[workflow-detail-handler] Error fetching workflow detail',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/workflow-detail-handler.ts"],"names":[],"mappings":";;;AA6DA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;AC3DA,IAAO,kCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EACuB;AACvB,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,EAAA,EAAG,GAAI,KAAA,CAAM,MAAA;AAErB,IAAA,IAAI,CAAC,EAAA,EAAI;AACP,MAAA,MAAM,IAAI,MAAM,sBAAsB,CAAA;AAAA,IACxC;AAEA,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,kBAAA,EAAqB,kBAAA,CAAmB,EAAE,CAAC,CAAA,CAAA;AACnE,IAAA,GAAA,CAAI,SAAS,MAAA,CAAO,IAAA,CAAK,+CAA+C,EAAE,CAAA,MAAA,EAAS,GAAG,CAAA,CAAE,CAAA;AAExF,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,UAAA,MAAM,IAAI,MAAM,oBAAoB,CAAA;AAAA,QACtC;AACA,QAAA,MAAM,IAAI,MAAM,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAEA,MAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,IAAA,EAAK;AAEpC,MAAA,IAAI,CAAC,MAAA,CAAO,EAAA,IAAM,CAAC,OAAO,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,0BAA0B,CAAA;AAAA,MAC5D;AAEA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,0DAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"workflow-detail-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * @module @kb-labs/workflow-cli/rest/workflow-detail-handler\n * REST handler for getting workflow definition details\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport type { WorkflowInfo } from '@kb-labs/workflow-contracts';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\ninterface WorkflowDetailParams {\n id: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, unknown, WorkflowDetailParams>\n ): Promise<WorkflowInfo> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { id } = input.params!;\n\n if (!id) {\n throw new Error('Missing id parameter');\n }\n\n const url = `${daemonUrl}/api/v1/workflows/${encodeURIComponent(id)}`;\n ctx.platform.logger.info(`[workflow-detail-handler] Fetching workflow ${id} from ${url}`);\n\n try {\n const response = await fetch(url);\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new Error('Workflow not found');\n }\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as { ok: boolean; data?: WorkflowInfo; error?: string };\n\n if (!result.ok || !result.data) {\n throw new Error(result.error || 'Failed to fetch workflow');\n }\n\n return result.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[workflow-detail-handler] Error fetching workflow detail',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/workflow-run-cancel-handler.ts"],"names":[],"mappings":";;;AAMA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACMA,IAAO,sCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EAC4B;AAC5B,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,KAAA,CAAM,MAAA;AAExB,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAAA,IAC3C;AAEA,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,uBAAA,EAA0B,kBAAA,CAAmB,KAAK,CAAC,CAAA,OAAA,CAAA;AAC3E,IAAA,GAAA,CAAI,SAAS,MAAA,CAAO,IAAA,CAAK,gDAAgD,KAAK,CAAA,IAAA,EAAO,GAAG,CAAA,CAAE,CAAA;AAE1F,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA;AAAmB,OAC/C,CAAA;AAED,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK,CAAE,MAAM,OAAO,EAAE,KAAA,EAAO,eAAA,EAAgB,CAAE,CAAA;AAChF,QAAA,MAAM,IAAI,KAAA,CAAM,SAAA,CAAU,KAAA,IAAS,CAAA,qBAAA,EAAwB,KAAK,CAAA,CAAE,CAAA;AAAA,MACpE;AAEA,MAAA,MAAM,OAAA,GAAW,MAAM,QAAA,CAAS,IAAA,EAAK;AACrC,MAAA,IAAI,CAAC,OAAA,CAAQ,EAAA,IAAM,CAAC,QAAQ,IAAA,EAAM;AAChC,QAAA,MAAM,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAA,IAAS,CAAA,qBAAA,EAAwB,KAAK,CAAA,CAAE,CAAA;AAAA,MAClE;AACA,MAAA,OAAO,OAAA,CAAQ,IAAA;AAAA,IACjB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,oDAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"workflow-run-cancel-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type { WorkflowRunRequest } from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as any).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics\n */\n async getMetrics(): Promise<any> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<any> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<any[]> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ logs: any[] }>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<any[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n const unwrapped = this.unwrapData<{ jobs?: any[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job: any) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<{\n crons: any[];\n }> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<any>(response);\n return this.unwrapData(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<any>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: POST /workflows/runs/:runId/cancel\n * Proxies to workflow daemon POST /api/v1/workflows/runs/:runId/cancel\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\ninterface RunCancelParams {\n runId: string;\n}\n\ninterface RunCancelResponse {\n cancelled: boolean;\n runId: string;\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, unknown, RunCancelParams>\n ): Promise<RunCancelResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { runId } = input.params!;\n\n if (!runId) {\n throw new Error('Missing runId parameter');\n }\n\n const url = `${daemonUrl}/api/v1/workflows/runs/${encodeURIComponent(runId)}/cancel`;\n ctx.platform.logger.info(`[workflow-run-cancel-handler] Cancelling run ${runId} at ${url}`);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({ error: 'Unknown error' })) as { error?: string };\n throw new Error(errorData.error || `Failed to cancel run ${runId}`);\n }\n\n const payload = (await response.json()) as ApiEnvelope<RunCancelResponse>;\n if (!payload.ok || !payload.data) {\n throw new Error(payload.error || `Failed to cancel run ${runId}`);\n }\n return payload.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[workflow-run-cancel-handler] Error cancelling run',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/http-client.ts","../../src/rest/workflow-run-cancel-handler.ts"],"names":[],"mappings":";;;AA6DA,IAAM,kBAAA,GAAqB,uBAAA;AASpB,SAAS,oBAAA,GAA+B;AAC7C,EAAA,OAAO,MAAA,CAAO,qBAAqB,CAAA,IAAK,kBAAA;AAC1C;;;ACjDA,IAAO,sCAAQ,aAAA,CAAc;AAAA,EAC3B,MAAM,OAAA,CACJ,GAAA,EACA,KAAA,EAC4B;AAC5B,IAAA,MAAM,YAAY,oBAAA,EAAqB;AACvC,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,KAAA,CAAM,MAAA;AAExB,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAAA,IAC3C;AAEA,IAAA,MAAM,MAAM,CAAA,EAAG,SAAS,CAAA,uBAAA,EAA0B,kBAAA,CAAmB,KAAK,CAAC,CAAA,OAAA,CAAA;AAC3E,IAAA,GAAA,CAAI,SAAS,MAAA,CAAO,IAAA,CAAK,gDAAgD,KAAK,CAAA,IAAA,EAAO,GAAG,CAAA,CAAE,CAAA;AAE1F,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA;AAAmB,OAC/C,CAAA;AAED,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK,CAAE,MAAM,OAAO,EAAE,KAAA,EAAO,eAAA,EAAgB,CAAE,CAAA;AAChF,QAAA,MAAM,IAAI,KAAA,CAAM,SAAA,CAAU,KAAA,IAAS,CAAA,qBAAA,EAAwB,KAAK,CAAA,CAAE,CAAA;AAAA,MACpE;AAEA,MAAA,MAAM,OAAA,GAAW,MAAM,QAAA,CAAS,IAAA,EAAK;AACrC,MAAA,IAAI,CAAC,OAAA,CAAQ,EAAA,IAAM,CAAC,QAAQ,IAAA,EAAM;AAChC,QAAA,MAAM,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAA,IAAS,CAAA,qBAAA,EAAwB,KAAK,CAAA,CAAE,CAAA;AAAA,MAClE;AACA,MAAA,OAAO,OAAA,CAAQ,IAAA;AAAA,IACjB,SAAS,KAAA,EAAO;AACd,MAAA,GAAA,CAAI,SAAS,MAAA,CAAO,KAAA;AAAA,QAClB,oDAAA;AAAA,QACA,KAAA,YAAiB,QAAQ,KAAA,GAAQ;AAAA,OACnC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AACF,CAAC","file":"workflow-run-cancel-handler.js","sourcesContent":["/**\n * HTTP client for interacting with Workflow Daemon\n */\nimport type {\n WorkflowRunRequest,\n JobStatusInfo,\n JobStepsResponse,\n JobLogsResponse,\n CronListResponse,\n} from '@kb-labs/workflow-contracts';\nimport { useEnv } from '@kb-labs/sdk';\n\n/** Metrics data returned by GET /metrics */\nexport interface WorkflowMetricsData {\n runs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n cancelled: number;\n };\n jobs: {\n total: number;\n queued: number;\n running: number;\n completed: number;\n failed: number;\n };\n [key: string]: unknown;\n}\n\n/** Extended job status returned by GET /api/v1/jobs/:id — includes nested jobs/steps */\nexport interface JobStatusDetail extends Omit<JobStatusInfo, 'result'> {\n result?: {\n ok: boolean;\n summary?: string;\n error?: { code: string; message: string };\n };\n jobs?: Array<{\n id: string;\n name: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n error?: string;\n steps?: Array<{\n id: string;\n name: string;\n status: string;\n handler?: string;\n startedAt?: string;\n finishedAt?: string;\n durationMs?: number;\n outputs?: Record<string, unknown>;\n error?: { message?: string } | string;\n }>;\n }>;\n}\n\nconst DEFAULT_DAEMON_URL = 'http://localhost:7778';\n\nexport interface DaemonClientOptions {\n url?: string;\n}\n\n/**\n * Get workflow daemon URL from environment or default\n */\nexport function getWorkflowDaemonUrl(): string {\n return useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n}\n\nexport class WorkflowDaemonClient {\n private readonly baseUrl: string;\n\n constructor(options: DaemonClientOptions = {}) {\n this.baseUrl = options.url ?? useEnv('WORKFLOW_DAEMON_URL') ?? DEFAULT_DAEMON_URL;\n }\n\n /**\n * Validate response Content-Type and parse JSON safely\n */\n private async parseJsonResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type');\n if (!contentType?.includes('application/json')) {\n throw new Error(`Invalid Content-Type: expected application/json, got ${contentType}`);\n }\n return response.json() as Promise<T>;\n }\n\n private unwrapData<T>(payload: unknown): T {\n if (\n payload\n && typeof payload === 'object'\n && 'ok' in payload\n && (payload as { ok?: boolean }).ok === true\n && 'data' in payload\n ) {\n return (payload as { data: T }).data;\n }\n return payload as T;\n }\n\n /**\n * Validate and encode job ID to prevent path traversal attacks\n */\n private validateAndEncodeJobId(jobId: string): string {\n // Validate job ID format (alphanumeric, hyphens, underscores only)\n if (!/^[a-zA-Z0-9_-]+$/.test(jobId)) {\n throw new Error(`Invalid job ID format: ${jobId}`);\n }\n // Encode for URL safety (defense in depth)\n return encodeURIComponent(jobId);\n }\n\n /**\n * Health check\n */\n async health(): Promise<{ ok: boolean; service: string }> {\n const response = await fetch(`${this.baseUrl}/health`);\n if (!response.ok) {\n throw new Error(`Health check failed: ${response.statusText}`);\n }\n return this.parseJsonResponse(response);\n }\n\n /**\n * Get workflow metrics (raw metrics data from daemon)\n */\n async getMetrics(): Promise<WorkflowMetricsData> {\n const response = await fetch(`${this.baseUrl}/metrics`);\n if (!response.ok) {\n throw new Error(`Failed to get metrics: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<WorkflowMetricsData>(data);\n }\n\n /**\n * Get job status with full details (jobs, steps, outputs)\n */\n async getJobStatus(jobId: string): Promise<JobStatusDetail> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job status: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStatusDetail>(data);\n }\n\n /**\n * Get job steps with outputs\n */\n async getJobSteps(jobId: string): Promise<JobStepsResponse> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/steps`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job steps: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<JobStepsResponse>(data);\n }\n\n /**\n * Get job logs\n */\n async getJobLogs(jobId: string): Promise<JobLogsResponse['logs']> {\n const encodedJobId = this.validateAndEncodeJobId(jobId);\n const response = await fetch(`${this.baseUrl}/api/v1/jobs/${encodedJobId}/logs`);\n if (response.status === 404) {\n throw new Error(`Job ${jobId} not found`);\n }\n if (!response.ok) {\n throw new Error(`Failed to get job logs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<JobLogsResponse>(data);\n return unwrapped.logs ?? [];\n }\n\n /**\n * Get active executions\n */\n async getExecutions(): Promise<JobStatusInfo[]> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`);\n if (!response.ok) {\n throw new Error(`Failed to get executions: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n const unwrapped = this.unwrapData<{ jobs?: JobStatusInfo[] }>(data);\n const jobs = unwrapped.jobs ?? [];\n return jobs.filter((job) => job.status === 'running' || job.status === 'pending');\n }\n\n /**\n * Get cron jobs\n */\n async getCronJobs(): Promise<CronListResponse> {\n const response = await fetch(`${this.baseUrl}/api/v1/cron`);\n if (!response.ok) {\n throw new Error(`Failed to get cron jobs: ${response.statusText}`);\n }\n const data = await this.parseJsonResponse<unknown>(response);\n return this.unwrapData<CronListResponse>(data);\n }\n\n /**\n * Submit a job for execution\n */\n async submitJob(params: {\n handler: string;\n input?: unknown;\n priority?: number;\n }): Promise<{ id: string; status: string }> {\n const response = await fetch(`${this.baseUrl}/api/v1/jobs`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n type: params.handler,\n payload: params.input,\n priority: params.priority,\n }),\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to submit job: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<unknown>(response);\n const data = this.unwrapData<{ jobId: string }>(payload);\n return { id: data.jobId, status: 'pending' };\n }\n\n /**\n * Run workflow by ID\n */\n async runWorkflow(\n workflowId: string,\n request: WorkflowRunRequest = {}\n ): Promise<{ runId: string; status: string }> {\n const response = await fetch(\n `${this.baseUrl}/api/v1/workflows/${encodeURIComponent(workflowId)}/runs`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n }\n );\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({ error: response.statusText }))) as {\n error?: string;\n };\n throw new Error(error.error || `Failed to run workflow: ${response.statusText}`);\n }\n\n const payload = await this.parseJsonResponse<{ ok: boolean; data?: { runId: string; status: string } }>(response);\n return this.unwrapData<{ runId: string; status: string }>(payload);\n }\n}\n","/**\n * REST handler: POST /workflows/runs/:runId/cancel\n * Proxies to workflow daemon POST /api/v1/workflows/runs/:runId/cancel\n */\n\nimport { defineHandler, type RestInput, type PluginContextV3 } from '@kb-labs/sdk';\nimport { getWorkflowDaemonUrl } from '../http-client';\n\ninterface RunCancelParams {\n runId: string;\n}\n\ninterface RunCancelResponse {\n cancelled: boolean;\n runId: string;\n}\n\ninterface ApiEnvelope<T> {\n ok: boolean;\n data?: T;\n error?: string;\n}\n\nexport default defineHandler({\n async execute(\n ctx: PluginContextV3,\n input: RestInput<unknown, unknown, RunCancelParams>\n ): Promise<RunCancelResponse> {\n const daemonUrl = getWorkflowDaemonUrl();\n const { runId } = input.params!;\n\n if (!runId) {\n throw new Error('Missing runId parameter');\n }\n\n const url = `${daemonUrl}/api/v1/workflows/runs/${encodeURIComponent(runId)}/cancel`;\n ctx.platform.logger.info(`[workflow-run-cancel-handler] Cancelling run ${runId} at ${url}`);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({ error: 'Unknown error' })) as { error?: string };\n throw new Error(errorData.error || `Failed to cancel run ${runId}`);\n }\n\n const payload = (await response.json()) as ApiEnvelope<RunCancelResponse>;\n if (!payload.ok || !payload.data) {\n throw new Error(payload.error || `Failed to cancel run ${runId}`);\n }\n return payload.data;\n } catch (error) {\n ctx.platform.logger.error(\n '[workflow-run-cancel-handler] Error cancelling run',\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n },\n});\n"]}
|