diff --git a/.changeset/better-rooms-fold.md b/.changeset/better-rooms-fold.md new file mode 100644 index 0000000000..d1490a8022 --- /dev/null +++ b/.changeset/better-rooms-fold.md @@ -0,0 +1,7 @@ +--- +"miniflare": minor +--- + +local explorer: serve the local explorer's OpenAPI spec at /cdn-cgi/explorer/api + +The local explorer is supported by a REST API served from the worker's local address. It can be accessed independently of the UI, (e.g. by an AI agent) and is thus documented at this endpoint. diff --git a/packages/miniflare/scripts/filter-openapi.ts b/packages/miniflare/scripts/filter-openapi.ts index d049fc26a0..15e7712c32 100644 --- a/packages/miniflare/scripts/filter-openapi.ts +++ b/packages/miniflare/scripts/filter-openapi.ts @@ -32,7 +32,7 @@ const DEFAULT_OUTPUT_PATH = join( const LOCAL_EXPLORER_INFO = { title: "Local Explorer API", description: - "Local subset of Cloudflare API for exploring resources during local development.", + "A local subset of the Cloudflare API for inspecting and modifying resource state during local development. Supports D1, R2, KV, Durable Objects and Workflows.", version: "0.0.1", }; diff --git a/packages/miniflare/scripts/openapi-filter-config.ts b/packages/miniflare/scripts/openapi-filter-config.ts index e8628e39e6..4edd2ce3fb 100644 --- a/packages/miniflare/scripts/openapi-filter-config.ts +++ b/packages/miniflare/scripts/openapi-filter-config.ts @@ -617,6 +617,640 @@ const config = { tags: ["Local Explorer"], }, }, + + // Workflows endpoints (local-only, not pulling from upstream API) + "/workflows": { + get: { + description: + "Returns the workflows configured for local development.", + operationId: "workflows-list-workflows", + parameters: [], + responses: { + "200": { + content: { + "application/json": { + schema: { + allOf: [ + { + $ref: "#/components/schemas/workers_api-response-common", + }, + { + properties: { + result: { + items: { + $ref: "#/components/schemas/workflows_workflow", + }, + type: "array", + }, + result_info: { + properties: { + count: { + type: "number", + }, + }, + type: "object", + }, + }, + type: "object", + }, + ], + }, + }, + }, + description: "List Workflows response.", + }, + "4XX": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/workers_api-response-common-failure", + }, + }, + }, + description: "List Workflows response failure.", + }, + }, + summary: "List Workflows", + tags: ["Workflows"], + }, + }, + "/workflows/{workflow_name}": { + get: { + description: + "Returns details of a specific workflow including instance status counts.", + operationId: "workflows-get-workflow-details", + parameters: [ + { + in: "path", + name: "workflow_name", + required: true, + schema: { + $ref: "#/components/schemas/workflows_workflow-name", + }, + }, + ], + responses: { + "200": { + content: { + "application/json": { + schema: { + allOf: [ + { + $ref: "#/components/schemas/workers_api-response-common", + }, + { + properties: { + result: { + $ref: "#/components/schemas/workflows_workflow-details", + }, + }, + type: "object", + }, + ], + }, + }, + }, + description: "Get Workflow Details response.", + }, + "4XX": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/workers_api-response-common-failure", + }, + }, + }, + description: "Get Workflow Details response failure.", + }, + }, + summary: "Get Workflow Details", + tags: ["Workflows"], + }, + delete: { + description: + "Deletes all instances of a workflow by removing their persistence files.", + operationId: "workflows-delete-workflow", + parameters: [ + { + in: "path", + name: "workflow_name", + required: true, + schema: { + $ref: "#/components/schemas/workflows_workflow-name", + }, + }, + ], + responses: { + "200": { + content: { + "application/json": { + schema: { + allOf: [ + { + $ref: "#/components/schemas/workers_api-response-common", + }, + { + properties: { + result: { + type: "object", + properties: { + status: { + type: "string", + }, + success: { + type: "boolean", + }, + }, + }, + }, + type: "object", + }, + ], + }, + }, + }, + description: "Delete Workflow response.", + }, + "4XX": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/workers_api-response-common-failure", + }, + }, + }, + description: "Delete Workflow response failure.", + }, + }, + summary: "Delete Workflow (all instances)", + tags: ["Workflows"], + }, + }, + "/workflows/{workflow_name}/instances": { + get: { + description: "Returns the instances of a workflow.", + operationId: "workflows-list-instances", + parameters: [ + { + in: "path", + name: "workflow_name", + required: true, + schema: { + $ref: "#/components/schemas/workflows_workflow-name", + }, + }, + { + in: "query", + name: "page", + schema: { + default: 1, + description: "Page number (1-indexed).", + minimum: 1, + type: "number", + }, + }, + { + in: "query", + name: "per_page", + schema: { + default: 25, + description: "Number of instances per page.", + maximum: 100, + minimum: 1, + type: "number", + }, + }, + { + in: "query", + name: "status", + schema: { + type: "string", + enum: [ + "queued", + "running", + "paused", + "errored", + "terminated", + "complete", + "waitingForPause", + "waiting", + ], + description: "Filter instances by status.", + }, + }, + ], + responses: { + "200": { + content: { + "application/json": { + schema: { + allOf: [ + { + $ref: "#/components/schemas/workers_api-response-common", + }, + { + properties: { + result: { + items: { + $ref: "#/components/schemas/workflows_instance", + }, + type: "array", + }, + result_info: { + properties: { + page: { + type: "number", + }, + per_page: { + type: "number", + }, + total_count: { + type: "number", + }, + total_pages: { + type: "number", + }, + }, + type: "object", + }, + }, + type: "object", + }, + ], + }, + }, + }, + description: "List Workflow Instances response.", + }, + "4XX": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/workers_api-response-common-failure", + }, + }, + }, + description: "List Workflow Instances response failure.", + }, + }, + summary: "List Workflow Instances", + tags: ["Workflows"], + }, + post: { + description: "Creates a new workflow instance.", + operationId: "workflows-create-instance", + parameters: [ + { + in: "path", + name: "workflow_name", + required: true, + schema: { + $ref: "#/components/schemas/workflows_workflow-name", + }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + id: { + type: "string", + description: + "Optional instance ID. If not provided, a UUID is generated.", + }, + params: { + description: + "Optional JSON payload to pass to the workflow.", + }, + }, + }, + }, + }, + }, + responses: { + "200": { + content: { + "application/json": { + schema: { + allOf: [ + { + $ref: "#/components/schemas/workers_api-response-common", + }, + { + properties: { + result: { + type: "object", + properties: { + id: { + type: "string", + description: + "The instance ID of the newly created workflow instance.", + }, + }, + required: ["id"], + }, + }, + type: "object", + }, + ], + }, + }, + }, + description: "Create Workflow Instance response.", + }, + "4XX": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/workers_api-response-common-failure", + }, + }, + }, + description: "Create Workflow Instance response failure.", + }, + }, + summary: "Create Workflow Instance", + tags: ["Workflows"], + }, + }, + "/workflows/{workflow_name}/instances/{instance_id}": { + get: { + description: "Returns the status details of a workflow instance.", + operationId: "workflows-get-instance-details", + parameters: [ + { + in: "path", + name: "workflow_name", + required: true, + schema: { + $ref: "#/components/schemas/workflows_workflow-name", + }, + }, + { + in: "path", + name: "instance_id", + required: true, + schema: { + $ref: "#/components/schemas/workflows_instance-id", + }, + }, + ], + responses: { + "200": { + content: { + "application/json": { + schema: { + allOf: [ + { + $ref: "#/components/schemas/workers_api-response-common", + }, + { + properties: { + result: { + $ref: "#/components/schemas/workflows_instance-details", + }, + }, + type: "object", + }, + ], + }, + }, + }, + description: "Get Workflow Instance Details response.", + }, + "4XX": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/workers_api-response-common-failure", + }, + }, + }, + description: "Get Workflow Instance Details response failure.", + }, + }, + summary: "Get Workflow Instance Details", + tags: ["Workflows"], + }, + delete: { + description: + "Deletes a workflow instance by removing its persistence files.", + operationId: "workflows-delete-instance", + parameters: [ + { + in: "path", + name: "workflow_name", + required: true, + schema: { + $ref: "#/components/schemas/workflows_workflow-name", + }, + }, + { + in: "path", + name: "instance_id", + required: true, + schema: { + $ref: "#/components/schemas/workflows_instance-id", + }, + }, + ], + responses: { + "200": { + content: { + "application/json": { + schema: { + allOf: [ + { + $ref: "#/components/schemas/workers_api-response-common", + }, + { + properties: { + result: { + type: "object", + properties: { + success: { + type: "boolean", + }, + }, + }, + }, + type: "object", + }, + ], + }, + }, + }, + description: "Delete Workflow Instance response.", + }, + "4XX": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/workers_api-response-common-failure", + }, + }, + }, + description: "Delete Workflow Instance response failure.", + }, + }, + summary: "Delete Workflow Instance", + tags: ["Workflows"], + }, + }, + "/workflows/{workflow_name}/instances/{instance_id}/status": { + patch: { + description: + "Changes the status of a workflow instance (pause, resume, restart, terminate).", + operationId: "workflows-change-instance-status", + parameters: [ + { + in: "path", + name: "workflow_name", + required: true, + schema: { + $ref: "#/components/schemas/workflows_workflow-name", + }, + }, + { + in: "path", + name: "instance_id", + required: true, + schema: { + $ref: "#/components/schemas/workflows_instance-id", + }, + }, + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + required: ["action"], + properties: { + action: { + type: "string", + enum: ["pause", "resume", "restart", "terminate"], + description: + "The action to perform on the workflow instance.", + }, + }, + }, + }, + }, + }, + responses: { + "200": { + content: { + "application/json": { + schema: { + allOf: [ + { + $ref: "#/components/schemas/workers_api-response-common", + }, + { + properties: { + result: { + type: "object", + properties: { + success: { + type: "boolean", + }, + }, + }, + }, + type: "object", + }, + ], + }, + }, + }, + description: "Change Workflow Instance Status response.", + }, + "4XX": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/workers_api-response-common-failure", + }, + }, + }, + description: "Change Workflow Instance Status response failure.", + }, + }, + summary: "Change Workflow Instance Status", + tags: ["Workflows"], + }, + }, + "/workflows/{workflow_name}/instances/{instance_id}/events/{event_type}": + { + post: { + description: "Sends an event to a workflow instance.", + operationId: "workflows-send-instance-event", + parameters: [ + { + in: "path", + name: "workflow_name", + required: true, + schema: { + $ref: "#/components/schemas/workflows_workflow-name", + }, + }, + { + in: "path", + name: "instance_id", + required: true, + schema: { + $ref: "#/components/schemas/workflows_instance-id", + }, + }, + { + in: "path", + name: "event_type", + required: true, + schema: { + type: "string", + description: "The event type to send.", + }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + description: "Optional JSON payload for the event.", + }, + }, + }, + }, + responses: { + "200": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/workers_api-response-common", + }, + }, + }, + description: "Send Event response.", + }, + "4XX": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/workers_api-response-common-failure", + }, + }, + }, + description: "Send Event response failure.", + }, + }, + summary: "Send Event to Workflow Instance", + tags: ["Workflows"], + }, + }, }, schemas: { // R2 schemas - matches stratus dashboard API shapes @@ -827,6 +1461,133 @@ const config = { }, }, }, + + // Workflows schemas (local-only, not pulling from upstream API) + "workflows_workflow-name": { + description: "The name of the workflow.", + example: "my-workflow", + type: "string", + }, + "workflows_instance-id": { + description: "The unique identifier of a workflow instance.", + example: "my-instance-id", + type: "string", + }, + workflows_workflow: { + type: "object", + properties: { + name: { + type: "string", + description: "The name of the workflow.", + }, + class_name: { + type: "string", + description: "The entrypoint class name of the workflow.", + }, + script_name: { + type: "string", + description: "The script name containing the workflow.", + }, + }, + required: ["name"], + }, + "workflows_workflow-details": { + type: "object", + properties: { + name: { + type: "string", + description: "The name of the workflow.", + }, + class_name: { + type: "string", + description: "The entrypoint class name.", + }, + script_name: { + type: "string", + description: "The script containing the workflow.", + }, + instances: { + type: "object", + description: "Instance counts by status.", + properties: { + complete: { type: "number" }, + errored: { type: "number" }, + paused: { type: "number" }, + queued: { type: "number" }, + running: { type: "number" }, + terminated: { type: "number" }, + waiting: { type: "number" }, + waitingForPause: { type: "number" }, + }, + }, + }, + required: ["name", "class_name", "script_name", "instances"], + }, + workflows_instance: { + type: "object", + properties: { + id: { + type: "string", + description: "The unique identifier of the workflow instance.", + }, + status: { + type: "string", + enum: [ + "queued", + "running", + "paused", + "errored", + "terminated", + "complete", + "waitingForPause", + "waiting", + "unknown", + ], + description: "The current status of the instance.", + }, + created_on: { + type: "string", + description: "ISO 8601 timestamp of when the instance was created.", + }, + }, + required: ["id"], + }, + "workflows_instance-details": { + type: "object", + properties: { + id: { + type: "string", + description: "The unique identifier of the workflow instance.", + }, + status: { + type: "string", + enum: [ + "queued", + "running", + "paused", + "errored", + "terminated", + "complete", + "waitingForPause", + "waiting", + "unknown", + ], + description: "The current status of the instance.", + }, + output: { + description: "Output value if the workflow completed successfully.", + }, + error: { + type: "object", + properties: { + name: { type: "string" }, + message: { type: "string" }, + }, + description: "Error details if the workflow errored.", + }, + }, + required: ["id", "status"], + }, }, }, } satisfies FilterConfig; diff --git a/packages/miniflare/src/workers/local-explorer/explorer.worker.ts b/packages/miniflare/src/workers/local-explorer/explorer.worker.ts index fe23288e9c..83acd048c8 100644 --- a/packages/miniflare/src/workers/local-explorer/explorer.worker.ts +++ b/packages/miniflare/src/workers/local-explorer/explorer.worker.ts @@ -18,6 +18,7 @@ import { zWorkersKvNamespaceListNamespacesData, zWorkflowsListInstancesData, } from "./generated/zod.gen"; +import openApiSpec from "./openapi.local.json"; import { listD1Databases, rawD1Database } from "./resources/d1"; import { listDONamespaces, listDOObjects, queryDOSqlite } from "./resources/do"; import { @@ -88,7 +89,8 @@ app.use("/api/*", async (c, next) => { status: 204, headers: { "Access-Control-Allow-Origin": origin ?? "*", - "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Methods": + "GET, POST, PUT, PATCH, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, cf-metadata-only, cf-r2-custom-metadata", "Access-Control-Max-Age": "86400", @@ -156,6 +158,12 @@ app.get("/*", async (c, next) => { return c.notFound(); }); +// ============================================================================ +// OpenAPI Spec Endpoint +// ============================================================================ + +app.get("/api", (c) => c.json(openApiSpec)); + // ============================================================================ // KV Endpoints // ============================================================================ diff --git a/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts b/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts index 12c788ef5f..5cf8dbdf51 100644 --- a/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts +++ b/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts @@ -590,6 +590,31 @@ export type LocalExplorerWorker = { protocol: string; }; +/** + * The name of the workflow. + */ +export type WorkflowsWorkflowName = string; + +/** + * The unique identifier of a workflow instance. + */ +export type WorkflowsInstanceId = string; + +export type WorkflowsWorkflow = { + /** + * The name of the workflow. + */ + name: string; + /** + * The entrypoint class name of the workflow. + */ + class_name?: string; + /** + * The script name containing the workflow. + */ + script_name?: string; +}; + export type WorkflowsWorkflowDetails = { /** * The name of the workflow. @@ -618,31 +643,6 @@ export type WorkflowsWorkflowDetails = { }; }; -/** - * The name of the workflow. - */ -export type WorkflowsWorkflowName = string; - -/** - * The unique identifier of a workflow instance. - */ -export type WorkflowsInstanceId = string; - -export type WorkflowsWorkflow = { - /** - * The name of the workflow. - */ - name: string; - /** - * The entrypoint class name of the workflow. - */ - class_name?: string; - /** - * The script name containing the workflow. - */ - script_name?: string; -}; - export type WorkflowsInstance = { /** * The unique identifier of the workflow instance. diff --git a/packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts b/packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts index dd5635df46..85a9edb4cc 100644 --- a/packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts +++ b/packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts @@ -409,22 +409,6 @@ export const zLocalExplorerWorker = z.object({ protocol: z.string(), }); -export const zWorkflowsWorkflowDetails = z.object({ - name: z.string(), - class_name: z.string(), - script_name: z.string(), - instances: z.object({ - complete: z.number().optional(), - errored: z.number().optional(), - paused: z.number().optional(), - queued: z.number().optional(), - running: z.number().optional(), - terminated: z.number().optional(), - waiting: z.number().optional(), - waitingForPause: z.number().optional(), - }), -}); - /** * The name of the workflow. */ @@ -441,6 +425,22 @@ export const zWorkflowsWorkflow = z.object({ script_name: z.string().optional(), }); +export const zWorkflowsWorkflowDetails = z.object({ + name: z.string(), + class_name: z.string(), + script_name: z.string(), + instances: z.object({ + complete: z.number().optional(), + errored: z.number().optional(), + paused: z.number().optional(), + queued: z.number().optional(), + running: z.number().optional(), + terminated: z.number().optional(), + waiting: z.number().optional(), + waitingForPause: z.number().optional(), + }), +}); + export const zWorkflowsInstance = z.object({ id: z.string(), status: z diff --git a/packages/miniflare/src/workers/local-explorer/openapi.local.json b/packages/miniflare/src/workers/local-explorer/openapi.local.json index a4fd1de84c..231beca5cb 100644 --- a/packages/miniflare/src/workers/local-explorer/openapi.local.json +++ b/packages/miniflare/src/workers/local-explorer/openapi.local.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "Local Explorer API", - "description": "Local subset of Cloudflare API for exploring resources during local development.", + "description": "A local subset of the Cloudflare API for inspecting and modifying resource state during local development. Supports D1, R2, KV, Durable Objects and Workflows.", "version": "0.0.1" }, "servers": [ @@ -1241,7 +1241,6 @@ "get": { "description": "List all workers in the local dev registry.", "operationId": "local-explorer-list-workers", - "parameters": [], "responses": { "200": { "content": { @@ -3010,6 +3009,34 @@ } } }, + "workflows_workflow-name": { + "description": "The name of the workflow.", + "example": "my-workflow", + "type": "string" + }, + "workflows_instance-id": { + "description": "The unique identifier of a workflow instance.", + "example": "my-instance-id", + "type": "string" + }, + "workflows_workflow": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the workflow." + }, + "class_name": { + "type": "string", + "description": "The entrypoint class name of the workflow." + }, + "script_name": { + "type": "string", + "description": "The script name containing the workflow." + } + }, + "required": ["name"] + }, "workflows_workflow-details": { "type": "object", "properties": { @@ -3058,34 +3085,6 @@ }, "required": ["name", "class_name", "script_name", "instances"] }, - "workflows_workflow-name": { - "description": "The name of the workflow.", - "example": "my-workflow", - "type": "string" - }, - "workflows_instance-id": { - "description": "The unique identifier of a workflow instance.", - "example": "my-instance-id", - "type": "string" - }, - "workflows_workflow": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The name of the workflow." - }, - "class_name": { - "type": "string", - "description": "The entrypoint class name of the workflow." - }, - "script_name": { - "type": "string", - "description": "The script name containing the workflow." - } - }, - "required": ["name"] - }, "workflows_instance": { "type": "object", "properties": { diff --git a/packages/miniflare/test/plugins/local-explorer/index.spec.ts b/packages/miniflare/test/plugins/local-explorer/index.spec.ts index 17c32335bd..c8be4f7937 100644 --- a/packages/miniflare/test/plugins/local-explorer/index.spec.ts +++ b/packages/miniflare/test/plugins/local-explorer/index.spec.ts @@ -217,7 +217,7 @@ describe("Local Explorer API validation", () => { "http://localhost:5173" ); expect(res.headers.get("Access-Control-Allow-Methods")).toBe( - "GET, POST, PUT, DELETE, OPTIONS" + "GET, POST, PUT, PATCH, DELETE, OPTIONS" ); await res.arrayBuffer(); @@ -255,6 +255,20 @@ describe("Local Explorer API validation", () => { }); describe("routing", () => { + test("serves OpenAPI spec at /cdn-cgi/explorer/api", async ({ expect }) => { + const res = await mf.dispatchFetch( + "http://localhost/cdn-cgi/explorer/api" + ); + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toContain("application/json"); + + const spec = await res.json(); + expect(spec).toMatchObject({ + openapi: "3.0.3", + info: { title: "Local Explorer API" }, + }); + }); + test("serves explorer UI at /cdn-cgi/explorer", async ({ expect }) => { const res = await mf.dispatchFetch("http://localhost/cdn-cgi/explorer"); expect(res.status).toBe(200);