@bratsos/workflow-engine-console 1.0.0-alpha.2
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/README.md +464 -0
- package/dist/actions-DOP8plrl.d.ts +52 -0
- package/dist/chunk-4RL6UAMS.js +486 -0
- package/dist/chunk-4RL6UAMS.js.map +1 -0
- package/dist/chunk-HUHLRNNM.js +517 -0
- package/dist/chunk-HUHLRNNM.js.map +1 -0
- package/dist/chunk-RCVZ3AP4.js +62 -0
- package/dist/chunk-RCVZ3AP4.js.map +1 -0
- package/dist/chunk-THOZTVDT.js +85 -0
- package/dist/chunk-THOZTVDT.js.map +1 -0
- package/dist/cli.d.ts +33 -0
- package/dist/cli.js +230 -0
- package/dist/cli.js.map +1 -0
- package/dist/handler-Cq2cVh7p.d.ts +47 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +15 -0
- package/dist/node.js +3 -0
- package/dist/node.js.map +1 -0
- package/dist/prisma-read-port-GPAHJEVC.js +4 -0
- package/dist/prisma-read-port-GPAHJEVC.js.map +1 -0
- package/dist/read-port-CRmaGAKb.d.ts +257 -0
- package/dist/testing.d.ts +50 -0
- package/dist/testing.js +346 -0
- package/dist/testing.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/node.ts
|
|
2
|
+
function toNodeHandler(handler) {
|
|
3
|
+
return async function nodeHandler(request, response) {
|
|
4
|
+
try {
|
|
5
|
+
const fetchRequest = await toFetchRequest(request);
|
|
6
|
+
const fetchResponse = await handler(fetchRequest);
|
|
7
|
+
response.statusCode = fetchResponse.status;
|
|
8
|
+
fetchResponse.headers.forEach((value, key) => {
|
|
9
|
+
response.setHeader(key, value);
|
|
10
|
+
});
|
|
11
|
+
const body = fetchResponse.body ? Buffer.from(await fetchResponse.arrayBuffer()) : null;
|
|
12
|
+
response.end(body ?? void 0);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
response.statusCode = 500;
|
|
15
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
16
|
+
response.end(
|
|
17
|
+
JSON.stringify({
|
|
18
|
+
error: {
|
|
19
|
+
code: "internal_error",
|
|
20
|
+
message: error instanceof Error ? error.message : "Unexpected error."
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async function toFetchRequest(request) {
|
|
28
|
+
const method = request.method ?? "GET";
|
|
29
|
+
const host = request.headers.host ?? "localhost";
|
|
30
|
+
const protocol = request.headers["x-forwarded-proto"] ?? "http";
|
|
31
|
+
const url = new URL(request.url ?? "/", `${protocol}://${host}`);
|
|
32
|
+
const headers = new Headers();
|
|
33
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
34
|
+
if (value === void 0) continue;
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
for (const entry of value) headers.append(key, entry);
|
|
37
|
+
} else {
|
|
38
|
+
headers.set(key, value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
42
|
+
const body = hasBody ? await readBody(request) : void 0;
|
|
43
|
+
return new Request(url, {
|
|
44
|
+
method,
|
|
45
|
+
headers,
|
|
46
|
+
body: body && body.length > 0 ? body : void 0
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function readBody(request) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const chunks = [];
|
|
52
|
+
request.on("data", (chunk) => {
|
|
53
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
54
|
+
});
|
|
55
|
+
request.on("end", () => resolve(Buffer.concat(chunks)));
|
|
56
|
+
request.on("error", reject);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { toNodeHandler };
|
|
61
|
+
//# sourceMappingURL=chunk-RCVZ3AP4.js.map
|
|
62
|
+
//# sourceMappingURL=chunk-RCVZ3AP4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/node.ts"],"names":[],"mappings":"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","file":"chunk-RCVZ3AP4.js","sourcesContent":["import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { WorkflowConsoleHandler } from \"./handler\";\n\n/**\n * Adapt the fetch handler to Node's `http` request/response pair.\n *\n * Only Express, Fastify and bare `node:http` need this. Next.js route\n * handlers, Hono, Cloudflare Workers, Deno and Bun all speak `Request` and\n * `Response` natively, so they mount the handler directly — see the README.\n */\nexport function toNodeHandler(\n handler: WorkflowConsoleHandler,\n): (request: IncomingMessage, response: ServerResponse) => Promise<void> {\n return async function nodeHandler(request, response) {\n try {\n const fetchRequest = await toFetchRequest(request);\n const fetchResponse = await handler(fetchRequest);\n response.statusCode = fetchResponse.status;\n fetchResponse.headers.forEach((value, key) => {\n response.setHeader(key, value);\n });\n const body = fetchResponse.body\n ? Buffer.from(await fetchResponse.arrayBuffer())\n : null;\n response.end(body ?? undefined);\n } catch (error) {\n response.statusCode = 500;\n response.setHeader(\"content-type\", \"application/json; charset=utf-8\");\n response.end(\n JSON.stringify({\n error: {\n code: \"internal_error\",\n message:\n error instanceof Error ? error.message : \"Unexpected error.\",\n },\n }),\n );\n }\n };\n}\n\nasync function toFetchRequest(request: IncomingMessage): Promise<Request> {\n const method = request.method ?? \"GET\";\n // `req.url` is path-and-query only; the Host header supplies the origin.\n // The origin is never used for routing — the handler derives its mount\n // path from the pathname — so a missing Host just needs a placeholder.\n const host = request.headers.host ?? \"localhost\";\n const protocol =\n (request.headers[\"x-forwarded-proto\"] as string | undefined) ?? \"http\";\n const url = new URL(request.url ?? \"/\", `${protocol}://${host}`);\n\n const headers = new Headers();\n for (const [key, value] of Object.entries(request.headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const entry of value) headers.append(key, entry);\n } else {\n headers.set(key, value);\n }\n }\n\n const hasBody = method !== \"GET\" && method !== \"HEAD\";\n const body = hasBody ? await readBody(request) : undefined;\n\n return new Request(url, {\n method,\n headers,\n body: body && body.length > 0 ? body : undefined,\n });\n}\n\nfunction readBody(request: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n request.on(\"data\", (chunk: Buffer | string) => {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n });\n request.on(\"end\", () => resolve(Buffer.concat(chunks)));\n request.on(\"error\", reject);\n });\n}\n"]}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var ConsoleQueryTimeoutError = class extends Error {
|
|
3
|
+
constructor(query, timeoutMs, options) {
|
|
4
|
+
super(
|
|
5
|
+
`Console query "${query}" was cancelled after ${timeoutMs}ms by statement_timeout. Narrow the filter or raise statementTimeoutMs.`,
|
|
6
|
+
options
|
|
7
|
+
);
|
|
8
|
+
this.query = query;
|
|
9
|
+
this.timeoutMs = timeoutMs;
|
|
10
|
+
this.name = "ConsoleQueryTimeoutError";
|
|
11
|
+
}
|
|
12
|
+
code = "query_timeout";
|
|
13
|
+
};
|
|
14
|
+
var STATEMENT_TIMEOUT_SQLSTATE = "57014";
|
|
15
|
+
function isStatementTimeout(error) {
|
|
16
|
+
if (typeof error !== "object" || error === null) return false;
|
|
17
|
+
const candidate = error;
|
|
18
|
+
if (candidate.code === STATEMENT_TIMEOUT_SQLSTATE) return true;
|
|
19
|
+
if (candidate.meta?.code === STATEMENT_TIMEOUT_SQLSTATE) return true;
|
|
20
|
+
const messages = [candidate.message, candidate.meta?.message];
|
|
21
|
+
return messages.some(
|
|
22
|
+
(message) => typeof message === "string" && (message.includes(STATEMENT_TIMEOUT_SQLSTATE) || message.includes("canceling statement due to statement timeout"))
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
var ConsoleBadRequestError = class extends Error {
|
|
26
|
+
code = "bad_request";
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "ConsoleBadRequestError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/read-port.ts
|
|
34
|
+
var CONSOLE_STATUSES = [
|
|
35
|
+
"PENDING",
|
|
36
|
+
"RUNNING",
|
|
37
|
+
"SUSPENDED",
|
|
38
|
+
"COMPLETED",
|
|
39
|
+
"FAILED",
|
|
40
|
+
"CANCELLED",
|
|
41
|
+
"SKIPPED"
|
|
42
|
+
];
|
|
43
|
+
function isConsoleStatus(value) {
|
|
44
|
+
return CONSOLE_STATUSES.includes(value);
|
|
45
|
+
}
|
|
46
|
+
var ALL_CAPABILITIES = {
|
|
47
|
+
runs: true,
|
|
48
|
+
steps: true,
|
|
49
|
+
annotations: true,
|
|
50
|
+
queue: true,
|
|
51
|
+
suspended: true,
|
|
52
|
+
deadLetters: true,
|
|
53
|
+
workers: true,
|
|
54
|
+
costs: true
|
|
55
|
+
};
|
|
56
|
+
var DEFAULT_RUN_LIMIT = 50;
|
|
57
|
+
var MAX_LIMIT = 200;
|
|
58
|
+
function clampLimit(requested, fallback = DEFAULT_RUN_LIMIT) {
|
|
59
|
+
if (requested === void 0 || !Number.isFinite(requested)) return fallback;
|
|
60
|
+
return Math.min(MAX_LIMIT, Math.max(1, Math.floor(requested)));
|
|
61
|
+
}
|
|
62
|
+
function encodeCursor(cursor) {
|
|
63
|
+
return Buffer.from(
|
|
64
|
+
`${cursor.createdAt.toISOString()}|${cursor.id}`,
|
|
65
|
+
"utf8"
|
|
66
|
+
).toString("base64url");
|
|
67
|
+
}
|
|
68
|
+
function decodeCursor(raw) {
|
|
69
|
+
let decoded;
|
|
70
|
+
try {
|
|
71
|
+
decoded = Buffer.from(raw, "base64url").toString("utf8");
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const separator = decoded.indexOf("|");
|
|
76
|
+
if (separator <= 0) return null;
|
|
77
|
+
const createdAt = new Date(decoded.slice(0, separator));
|
|
78
|
+
const id = decoded.slice(separator + 1);
|
|
79
|
+
if (Number.isNaN(createdAt.getTime()) || id.length === 0) return null;
|
|
80
|
+
return { createdAt, id };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { ALL_CAPABILITIES, CONSOLE_STATUSES, ConsoleBadRequestError, ConsoleQueryTimeoutError, DEFAULT_RUN_LIMIT, MAX_LIMIT, clampLimit, decodeCursor, encodeCursor, isConsoleStatus, isStatementTimeout };
|
|
84
|
+
//# sourceMappingURL=chunk-THOZTVDT.js.map
|
|
85
|
+
//# sourceMappingURL=chunk-THOZTVDT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/read-port.ts"],"names":[],"mappings":"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","file":"chunk-THOZTVDT.js","sourcesContent":["/**\n * A console query was cancelled by `statement_timeout` (Postgres `57014`).\n *\n * Thrown rather than swallowed so the handler can answer with an honest\n * \"that query was too expensive\" instead of holding the request open. The\n * UI renders it as an inline banner and leaves the filter bar interactive,\n * so the operator can narrow the filter and retry — a page that keeps\n * working is worth more than a spinner that never resolves.\n */\nexport class ConsoleQueryTimeoutError extends Error {\n readonly code = \"query_timeout\" as const;\n constructor(\n readonly query: string,\n readonly timeoutMs: number,\n options?: { cause?: unknown },\n ) {\n super(\n `Console query \"${query}\" was cancelled after ${timeoutMs}ms by statement_timeout. Narrow the filter or raise statementTimeoutMs.`,\n options,\n );\n this.name = \"ConsoleQueryTimeoutError\";\n }\n}\n\n/** Postgres `query_canceled`. */\nconst STATEMENT_TIMEOUT_SQLSTATE = \"57014\";\n\n/**\n * Recognise a cancelled statement through whatever wrapper the driver put\n * around it. Prisma surfaces the SQLSTATE on `meta.code` for\n * `PrismaClientKnownRequestError` P2010, node-postgres puts it on `code`,\n * and a raw error may only carry the message.\n */\nexport function isStatementTimeout(error: unknown): boolean {\n if (typeof error !== \"object\" || error === null) return false;\n const candidate = error as {\n code?: unknown;\n meta?: { code?: unknown; message?: unknown };\n message?: unknown;\n };\n if (candidate.code === STATEMENT_TIMEOUT_SQLSTATE) return true;\n if (candidate.meta?.code === STATEMENT_TIMEOUT_SQLSTATE) return true;\n const messages = [candidate.message, candidate.meta?.message];\n return messages.some(\n (message) =>\n typeof message === \"string\" &&\n (message.includes(STATEMENT_TIMEOUT_SQLSTATE) ||\n message.includes(\"canceling statement due to statement timeout\")),\n );\n}\n\n/** The request asked for something the console cannot serve. */\nexport class ConsoleBadRequestError extends Error {\n readonly code = \"bad_request\" as const;\n constructor(message: string) {\n super(message);\n this.name = \"ConsoleBadRequestError\";\n }\n}\n","/**\n * The console's read contract.\n *\n * Everything the console can show is expressed here, and nothing else in\n * the package reaches for a database. An implementation is handed the\n * connection its caller already has — a Prisma client, or the transaction\n * client the caller opened — and never opens one of its own. That is the\n * property the whole design exists to preserve: a consumer running the\n * kernel inside one transaction per tenant, under row-level security, gets\n * the console's reads inside that same transaction, evaluated against the\n * same policies, with authentication already decided upstream. A reader\n * that dialled the database itself would be a second session and therefore\n * a second security context, and no amount of configuration recovers the\n * tenant scoping once that happens.\n *\n * The interface is declared here rather than added to the engine's\n * `PersistenceCore` on purpose — see the note in the package README. It is\n * a small, optional surface; making it a required part of the persistence\n * port would break every third-party adapter for a feature that is\n * explicitly optional.\n */\n\n/** Run/stage/job lifecycle states, mirroring the engine's `Status` enum. */\nexport type ConsoleStatus =\n | \"PENDING\"\n | \"RUNNING\"\n | \"SUSPENDED\"\n | \"COMPLETED\"\n | \"FAILED\"\n | \"CANCELLED\"\n | \"SKIPPED\";\n\nexport const CONSOLE_STATUSES: readonly ConsoleStatus[] = [\n \"PENDING\",\n \"RUNNING\",\n \"SUSPENDED\",\n \"COMPLETED\",\n \"FAILED\",\n \"CANCELLED\",\n \"SKIPPED\",\n] as const;\n\nexport function isConsoleStatus(value: string): value is ConsoleStatus {\n return (CONSOLE_STATUSES as readonly string[]).includes(value);\n}\n\n/** One row of the runs list. Deliberately excludes `input`/`output`/`config`: a list page must not carry payloads. */\nexport interface RunSummary {\n id: string;\n createdAt: Date;\n updatedAt: Date;\n workflowId: string;\n workflowName: string;\n workflowType: string;\n status: ConsoleStatus;\n startedAt: Date | null;\n completedAt: Date | null;\n duration: number | null;\n totalCost: number;\n totalTokens: number;\n priority: number;\n definitionVersion: string | null;\n redriveCount: number;\n}\n\nexport interface StageSummary {\n id: string;\n workflowRunId: string;\n stageId: string;\n stageName: string;\n stageNumber: number;\n executionGroup: number;\n attempt: number;\n status: ConsoleStatus;\n startedAt: Date | null;\n completedAt: Date | null;\n duration: number | null;\n nextPollAt: Date | null;\n pollInterval: number | null;\n maxWaitUntil: Date | null;\n errorMessage: string | null;\n}\n\n/** A durable-step ledger entry. `status` is a free-form string in the schema, so it stays one here. */\nexport interface StepSummary {\n id: string;\n stageRecordId: string;\n stepId: string;\n seq: number;\n kind: string;\n status: string;\n attempt: number;\n leaseExpiresAt: Date | null;\n deadlineAt: Date | null;\n /** Deterministic name of the external effect a `run` step creates; null for other kinds. */\n externalKey: string | null;\n error: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface AnnotationSummary {\n id: string;\n createdAt: Date;\n workflowRunId: string;\n workflowStageRecordId: string | null;\n attempt: number;\n scope: string;\n scopeId: string | null;\n actorKind: string | null;\n actorId: string | null;\n key: string;\n value: unknown;\n}\n\nexport interface LogEntry {\n id: string;\n createdAt: Date;\n workflowRunId: string | null;\n workflowStageId: string | null;\n level: string;\n message: string;\n}\n\n/**\n * One retained outbox row, read as a run's event timeline. The engine marks\n * events published rather than deleting them, and `(workflowRunId, sequence)`\n * is unique, so a run timeline is an index range scan.\n */\nexport interface RunEvent {\n id: string;\n workflowRunId: string;\n sequence: number;\n eventType: string;\n occurredAt: Date;\n publishedAt: Date | null;\n retryCount: number;\n dlqAt: Date | null;\n}\n\nexport interface RunDetail {\n run: RunSummary & {\n input: unknown;\n output: unknown;\n config: unknown;\n metadata: unknown;\n };\n stages: StageSummary[];\n steps: StepSummary[];\n annotations: AnnotationSummary[];\n logs: LogEntry[];\n events: RunEvent[];\n /** True when a list was cut off by its limit, so the UI can say so instead of implying completeness. */\n truncated: {\n steps: boolean;\n annotations: boolean;\n logs: boolean;\n events: boolean;\n };\n}\n\nexport interface RunListFilters {\n status?: ConsoleStatus[];\n workflowId?: string;\n workflowType?: string;\n createdAfter?: Date;\n createdBefore?: Date;\n definitionVersion?: string;\n}\n\nexport interface RunListQuery {\n filters?: RunListFilters;\n /** Opaque cursor from a previous page's `nextCursor`. Keyset, not offset. */\n cursor?: string;\n limit?: number;\n}\n\nexport interface RunListPage {\n runs: RunSummary[];\n nextCursor: string | null;\n}\n\nexport interface QueueHealth {\n countsByStatus: Record<ConsoleStatus, number>;\n /** Enqueue time of the oldest job still waiting to be picked up. */\n oldestPendingAt: Date | null;\n /** `lockedAt` of the longest-held lease among running jobs — a lease older than your reaper's threshold is a stuck worker. */\n oldestLeaseAt: Date | null;\n /** Suspended jobs whose `nextPollAt` is already in the past. */\n overduePolls: number;\n}\n\n/**\n * A worker, derived from the leases it holds. The engine has no worker\n * registry table; `touchJob` refreshes `job_queue.lockedAt`, so the newest\n * lease a worker holds is the best available heartbeat, and a worker with\n * no running job is invisible here by construction.\n */\nexport interface WorkerInstance {\n workerId: string;\n runningJobs: number;\n oldestLockedAt: Date | null;\n /** Most recent `touchJob`; the console renders `now - lastSeenAt` as heartbeat age. */\n lastSeenAt: Date | null;\n}\n\nexport interface SuspendedStage {\n id: string;\n workflowRunId: string;\n workflowId: string;\n stageId: string;\n stageName: string;\n attempt: number;\n nextPollAt: Date | null;\n pollInterval: number | null;\n maxWaitUntil: Date | null;\n}\n\nexport interface DeadLetter {\n id: string;\n workflowRunId: string;\n sequence: number;\n eventType: string;\n retryCount: number;\n occurredAt: Date;\n dlqAt: Date | null;\n}\n\nexport interface CostBucket {\n /** A workflow id, or an ISO date (`YYYY-MM-DD`) when bucketing by day. */\n key: string;\n runs: number;\n cost: number;\n tokens: number;\n}\n\nexport interface CostQuery {\n by: \"workflow\" | \"day\";\n from?: Date;\n to?: Date;\n}\n\n/**\n * What a given reader can actually serve, so the UI hides what is not there\n * rather than rendering an empty page. Adapters that predate a view, or\n * schemas whose migrations lag, report `false` and the tab disappears.\n */\nexport interface ConsoleCapabilities {\n runs: boolean;\n steps: boolean;\n annotations: boolean;\n queue: boolean;\n suspended: boolean;\n deadLetters: boolean;\n workers: boolean;\n costs: boolean;\n}\n\nexport const ALL_CAPABILITIES: ConsoleCapabilities = {\n runs: true,\n steps: true,\n annotations: true,\n queue: true,\n suspended: true,\n deadLetters: true,\n workers: true,\n costs: true,\n};\n\nexport interface RunDetailOptions {\n stepLimit?: number;\n annotationLimit?: number;\n logLimit?: number;\n eventLimit?: number;\n}\n\nexport interface ConsoleReadPort {\n readonly capabilities: ConsoleCapabilities;\n\n listRuns(query: RunListQuery): Promise<RunListPage>;\n getRunDetail(\n runId: string,\n options?: RunDetailOptions,\n ): Promise<RunDetail | null>;\n /** Incremental tail of one run's timeline. `afterSequence` of 0 returns from the start. */\n listRunEvents(\n runId: string,\n afterSequence: number,\n limit?: number,\n ): Promise<RunEvent[]>;\n getQueueHealth(): Promise<QueueHealth>;\n listSuspendedStages(limit?: number): Promise<SuspendedStage[]>;\n listDeadLetters(limit?: number): Promise<DeadLetter[]>;\n listWorkers(): Promise<WorkerInstance[]>;\n getCosts(query: CostQuery): Promise<CostBucket[]>;\n}\n\n/** Default page size for the runs list. */\nexport const DEFAULT_RUN_LIMIT = 50;\n/** Hard ceiling on any page size, whatever the request asks for. */\nexport const MAX_LIMIT = 200;\n\nexport function clampLimit(\n requested: number | undefined,\n fallback = DEFAULT_RUN_LIMIT,\n): number {\n if (requested === undefined || !Number.isFinite(requested)) return fallback;\n return Math.min(MAX_LIMIT, Math.max(1, Math.floor(requested)));\n}\n\n/**\n * Keyset cursors are `(createdAt, id)` — the exact tuple the composite\n * indexes carry — encoded as opaque text so callers cannot turn one into an\n * offset. Offset pagination is what makes page 400 a sequential scan.\n */\nexport interface RunCursor {\n createdAt: Date;\n id: string;\n}\n\nexport function encodeCursor(cursor: RunCursor): string {\n return Buffer.from(\n `${cursor.createdAt.toISOString()}|${cursor.id}`,\n \"utf8\",\n ).toString(\"base64url\");\n}\n\nexport function decodeCursor(raw: string): RunCursor | null {\n let decoded: string;\n try {\n decoded = Buffer.from(raw, \"base64url\").toString(\"utf8\");\n } catch {\n return null;\n }\n const separator = decoded.indexOf(\"|\");\n if (separator <= 0) return null;\n const createdAt = new Date(decoded.slice(0, separator));\n const id = decoded.slice(separator + 1);\n if (Number.isNaN(createdAt.getTime()) || id.length === 0) return null;\n return { createdAt, id };\n}\n"]}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { C as ConsoleKernel } from './actions-DOP8plrl.js';
|
|
3
|
+
import { C as ConsoleReadPort } from './read-port-CRmaGAKb.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a `--config` module must default-export, or export as `console`:
|
|
7
|
+
* either the object or a zero-argument function returning it.
|
|
8
|
+
*
|
|
9
|
+
* Building it is the consumer's job precisely because they know how their
|
|
10
|
+
* client is constructed — which role it connects as, which `SET LOCAL`
|
|
11
|
+
* calls their RLS policies expect, whether it is a transaction client.
|
|
12
|
+
*/
|
|
13
|
+
interface ConsoleDevConfig {
|
|
14
|
+
reader: ConsoleReadPort;
|
|
15
|
+
kernel?: ConsoleKernel;
|
|
16
|
+
/** Called once when the server shuts down, to close whatever the module opened. */
|
|
17
|
+
dispose?: () => void | Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
interface CliOptions {
|
|
20
|
+
configPath?: string;
|
|
21
|
+
port: number;
|
|
22
|
+
host: string;
|
|
23
|
+
actions: boolean;
|
|
24
|
+
allowDirectConnection: boolean;
|
|
25
|
+
databaseUrl?: string;
|
|
26
|
+
pollIntervalMs: number;
|
|
27
|
+
help: boolean;
|
|
28
|
+
}
|
|
29
|
+
declare function parseArgs(argv: string[]): CliOptions;
|
|
30
|
+
declare function assertLoopback(host: string): void;
|
|
31
|
+
declare function main(argv: string[]): Promise<number>;
|
|
32
|
+
|
|
33
|
+
export { type ConsoleDevConfig, assertLoopback, main, parseArgs };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createWorkflowConsole } from './chunk-4RL6UAMS.js';
|
|
3
|
+
import { toNodeHandler } from './chunk-RCVZ3AP4.js';
|
|
4
|
+
import './chunk-THOZTVDT.js';
|
|
5
|
+
import { createServer } from 'http';
|
|
6
|
+
import { pathToFileURL } from 'url';
|
|
7
|
+
|
|
8
|
+
var HELP = `workflow-console \u2014 serve the workflow engine console locally
|
|
9
|
+
|
|
10
|
+
A DEVELOPMENT TOOL. It binds to loopback (127.0.0.1) and refuses any other
|
|
11
|
+
interface: it ships no authentication, so anything that can reach it can
|
|
12
|
+
read every run in the database.
|
|
13
|
+
|
|
14
|
+
Usage
|
|
15
|
+
workflow-console --config ./console.config.mjs [options]
|
|
16
|
+
workflow-console --database-url postgres://... --allow-direct-connection
|
|
17
|
+
|
|
18
|
+
Options
|
|
19
|
+
--config <path> Module that builds the reader the way your own
|
|
20
|
+
application does. Default-export (or export as
|
|
21
|
+
\`console\`) an object { reader, kernel?, dispose? },
|
|
22
|
+
or a function returning one. This is the path that
|
|
23
|
+
preserves row-level security, because your code
|
|
24
|
+
constructs the client.
|
|
25
|
+
--database-url <url> Connect directly with Prisma instead. Requires
|
|
26
|
+
--allow-direct-connection.
|
|
27
|
+
--allow-direct-connection Acknowledge that a direct connection is a second
|
|
28
|
+
session and therefore a second security context:
|
|
29
|
+
row-level security scoped to your application's
|
|
30
|
+
session will NOT apply, and this connection sees
|
|
31
|
+
whatever its role can see, across all tenants.
|
|
32
|
+
--port <n> Default 7799.
|
|
33
|
+
--host <addr> Default 127.0.0.1. Only loopback is accepted.
|
|
34
|
+
--actions Enable cancel / rerun / replay. Off by default;
|
|
35
|
+
needs a kernel from the --config module.
|
|
36
|
+
--poll <ms> UI poll interval. Default 5000.
|
|
37
|
+
--help This text.
|
|
38
|
+
`;
|
|
39
|
+
function parseArgs(argv) {
|
|
40
|
+
const options = {
|
|
41
|
+
port: 7799,
|
|
42
|
+
host: "127.0.0.1",
|
|
43
|
+
actions: false,
|
|
44
|
+
allowDirectConnection: false,
|
|
45
|
+
pollIntervalMs: 5e3,
|
|
46
|
+
help: false
|
|
47
|
+
};
|
|
48
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
49
|
+
const arg = argv[index];
|
|
50
|
+
const next = () => {
|
|
51
|
+
const value = argv[index + 1];
|
|
52
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
53
|
+
throw new Error(`${arg} needs a value.`);
|
|
54
|
+
}
|
|
55
|
+
index += 1;
|
|
56
|
+
return value;
|
|
57
|
+
};
|
|
58
|
+
switch (arg) {
|
|
59
|
+
case "--config":
|
|
60
|
+
options.configPath = next();
|
|
61
|
+
break;
|
|
62
|
+
case "--database-url":
|
|
63
|
+
options.databaseUrl = next();
|
|
64
|
+
break;
|
|
65
|
+
case "--allow-direct-connection":
|
|
66
|
+
options.allowDirectConnection = true;
|
|
67
|
+
break;
|
|
68
|
+
case "--port": {
|
|
69
|
+
const port = Number(next());
|
|
70
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
71
|
+
throw new Error("--port must be an integer between 1 and 65535.");
|
|
72
|
+
}
|
|
73
|
+
options.port = port;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "--host":
|
|
77
|
+
options.host = next();
|
|
78
|
+
break;
|
|
79
|
+
case "--actions":
|
|
80
|
+
options.actions = true;
|
|
81
|
+
break;
|
|
82
|
+
case "--poll": {
|
|
83
|
+
const poll = Number(next());
|
|
84
|
+
if (!Number.isFinite(poll) || poll < 0) {
|
|
85
|
+
throw new Error("--poll must be a non-negative number.");
|
|
86
|
+
}
|
|
87
|
+
options.pollIntervalMs = poll;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "--help":
|
|
91
|
+
case "-h":
|
|
92
|
+
options.help = true;
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`Unknown argument "${arg}". Try --help.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return options;
|
|
99
|
+
}
|
|
100
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
101
|
+
function assertLoopback(host) {
|
|
102
|
+
if (!LOOPBACK.has(host)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`--host ${host} is refused. This console ships no authentication, so it binds to loopback only. Put the mounted handler in your application if you need it reachable.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function loadConfigModule(path) {
|
|
109
|
+
const url = pathToFileURL(
|
|
110
|
+
path.startsWith("/") ? path : `${process.cwd()}/${path}`
|
|
111
|
+
).href;
|
|
112
|
+
const module = await import(url);
|
|
113
|
+
const candidate = module.default ?? module.console;
|
|
114
|
+
const resolved = typeof candidate === "function" ? await candidate() : candidate;
|
|
115
|
+
if (typeof resolved !== "object" || resolved === null || !("reader" in resolved)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`${path} must export { reader, kernel?, dispose? } as its default export (or as \`console\`), or a function returning it.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return resolved;
|
|
121
|
+
}
|
|
122
|
+
async function connectDirectly(databaseUrl) {
|
|
123
|
+
const { createPrismaConsoleReadPort } = await import('./prisma-read-port-GPAHJEVC.js');
|
|
124
|
+
const prismaSpecifier = "@prisma/client";
|
|
125
|
+
const prismaModule = await import(prismaSpecifier).catch(() => {
|
|
126
|
+
throw new Error(
|
|
127
|
+
"--database-url needs @prisma/client installed in this project."
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
const PrismaClient = prismaModule.PrismaClient;
|
|
131
|
+
if (!PrismaClient) {
|
|
132
|
+
throw new Error("@prisma/client did not export PrismaClient.");
|
|
133
|
+
}
|
|
134
|
+
const client = new PrismaClient({
|
|
135
|
+
datasources: { db: { url: databaseUrl } }
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
reader: createPrismaConsoleReadPort(
|
|
139
|
+
client
|
|
140
|
+
),
|
|
141
|
+
dispose: () => client.$disconnect()
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async function main(argv) {
|
|
145
|
+
let options;
|
|
146
|
+
try {
|
|
147
|
+
options = parseArgs(argv);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
process.stderr.write(
|
|
150
|
+
`${error instanceof Error ? error.message : String(error)}
|
|
151
|
+
`
|
|
152
|
+
);
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
if (options.help) {
|
|
156
|
+
process.stdout.write(HELP);
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
assertLoopback(options.host);
|
|
161
|
+
let config;
|
|
162
|
+
if (options.configPath) {
|
|
163
|
+
config = await loadConfigModule(options.configPath);
|
|
164
|
+
} else {
|
|
165
|
+
const databaseUrl = options.databaseUrl ?? process.env.DATABASE_URL;
|
|
166
|
+
if (!databaseUrl) {
|
|
167
|
+
process.stderr.write(
|
|
168
|
+
"Nothing to connect to. Pass --config <path> (preferred: your code builds the client, so row-level security still applies) or --database-url with --allow-direct-connection.\n"
|
|
169
|
+
);
|
|
170
|
+
return 2;
|
|
171
|
+
}
|
|
172
|
+
if (!options.allowDirectConnection) {
|
|
173
|
+
process.stderr.write(
|
|
174
|
+
"Refusing to open a database connection of my own.\n\nA direct connection is a second session and therefore a second security context: row-level security scoped to your application's session will not apply, and this console would read whatever its role can read, across every tenant.\n\nEither pass --config <path> to a module that builds the reader the way your own code does, or re-run with --allow-direct-connection to accept that.\n"
|
|
175
|
+
);
|
|
176
|
+
return 2;
|
|
177
|
+
}
|
|
178
|
+
config = await connectDirectly(databaseUrl);
|
|
179
|
+
}
|
|
180
|
+
if (options.actions && !config.kernel) {
|
|
181
|
+
process.stderr.write(
|
|
182
|
+
"--actions needs a kernel. Export one from your --config module; writes go through kernel commands, not SQL.\n"
|
|
183
|
+
);
|
|
184
|
+
await config.dispose?.();
|
|
185
|
+
return 2;
|
|
186
|
+
}
|
|
187
|
+
const handler = createWorkflowConsole({
|
|
188
|
+
reader: config.reader,
|
|
189
|
+
kernel: config.kernel,
|
|
190
|
+
actions: options.actions,
|
|
191
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
192
|
+
// Local, single-operator, already behind loopback: there is no
|
|
193
|
+
// principal to distinguish, so everything the console offers is
|
|
194
|
+
// allowed. The mounted handler defaults the other way, to deny.
|
|
195
|
+
authorize: () => true
|
|
196
|
+
});
|
|
197
|
+
const server = createServer(toNodeHandler(handler));
|
|
198
|
+
await new Promise((resolve) => {
|
|
199
|
+
server.listen(options.port, options.host, resolve);
|
|
200
|
+
});
|
|
201
|
+
process.stdout.write(
|
|
202
|
+
`Workflow console on http://${options.host}:${options.port}/
|
|
203
|
+
${options.actions ? "actions enabled" : "read-only"}, polling every ${options.pollIntervalMs}ms
|
|
204
|
+
Development tool: loopback only, no authentication. Do not expose it.
|
|
205
|
+
`
|
|
206
|
+
);
|
|
207
|
+
const shutdown = () => {
|
|
208
|
+
server.close(() => {
|
|
209
|
+
void Promise.resolve(config.dispose?.()).then(() => process.exit(0));
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
process.on("SIGINT", shutdown);
|
|
213
|
+
process.on("SIGTERM", shutdown);
|
|
214
|
+
return 0;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
`${error instanceof Error ? error.message : String(error)}
|
|
218
|
+
`
|
|
219
|
+
);
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
224
|
+
const code = await main(process.argv.slice(2));
|
|
225
|
+
if (code !== 0) process.exit(code);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export { assertLoopback, main, parseArgs };
|
|
229
|
+
//# sourceMappingURL=cli.js.map
|
|
230
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"names":[],"mappings":"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","file":"cli.js","sourcesContent":["#!/usr/bin/env node\n/**\n * `workflow-console` — serve the console locally, with no host application.\n *\n * The mounted handler assumes the consumer has a web app to mount into. A\n * worker-only repository has none, and that is a real gap rather than an\n * edge case. This command closes it with the *same* handler plus a server:\n * there is no second implementation of anything here.\n *\n * It is a development tool and binds to loopback only. The interesting\n * decision is where the database access comes from. The whole point of the\n * mounted design is that the console runs on the connection the consumer\n * already has, inside their transaction, under their row-level security —\n * so the default path here asks the consumer for a module that builds the\n * reader exactly the way their own code does. Falling back to a connection\n * string of our own would quietly discard that property, so it is not the\n * default: it requires an explicit acknowledgement.\n */\nimport { createServer } from \"node:http\";\nimport { pathToFileURL } from \"node:url\";\nimport type { ConsoleKernel } from \"./actions\";\nimport { createWorkflowConsole } from \"./handler\";\nimport { toNodeHandler } from \"./node\";\nimport type { ConsoleReadPort } from \"./read-port\";\n\n/**\n * What a `--config` module must default-export, or export as `console`:\n * either the object or a zero-argument function returning it.\n *\n * Building it is the consumer's job precisely because they know how their\n * client is constructed — which role it connects as, which `SET LOCAL`\n * calls their RLS policies expect, whether it is a transaction client.\n */\nexport interface ConsoleDevConfig {\n reader: ConsoleReadPort;\n kernel?: ConsoleKernel;\n /** Called once when the server shuts down, to close whatever the module opened. */\n dispose?: () => void | Promise<void>;\n}\n\ninterface CliOptions {\n configPath?: string;\n port: number;\n host: string;\n actions: boolean;\n allowDirectConnection: boolean;\n databaseUrl?: string;\n pollIntervalMs: number;\n help: boolean;\n}\n\nconst HELP = `workflow-console — serve the workflow engine console locally\n\n A DEVELOPMENT TOOL. It binds to loopback (127.0.0.1) and refuses any other\n interface: it ships no authentication, so anything that can reach it can\n read every run in the database.\n\nUsage\n workflow-console --config ./console.config.mjs [options]\n workflow-console --database-url postgres://... --allow-direct-connection\n\nOptions\n --config <path> Module that builds the reader the way your own\n application does. Default-export (or export as\n \\`console\\`) an object { reader, kernel?, dispose? },\n or a function returning one. This is the path that\n preserves row-level security, because your code\n constructs the client.\n --database-url <url> Connect directly with Prisma instead. Requires\n --allow-direct-connection.\n --allow-direct-connection Acknowledge that a direct connection is a second\n session and therefore a second security context:\n row-level security scoped to your application's\n session will NOT apply, and this connection sees\n whatever its role can see, across all tenants.\n --port <n> Default 7799.\n --host <addr> Default 127.0.0.1. Only loopback is accepted.\n --actions Enable cancel / rerun / replay. Off by default;\n needs a kernel from the --config module.\n --poll <ms> UI poll interval. Default 5000.\n --help This text.\n`;\n\nexport function parseArgs(argv: string[]): CliOptions {\n const options: CliOptions = {\n port: 7799,\n host: \"127.0.0.1\",\n actions: false,\n allowDirectConnection: false,\n pollIntervalMs: 5000,\n help: false,\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index];\n const next = (): string => {\n const value = argv[index + 1];\n if (value === undefined || value.startsWith(\"--\")) {\n throw new Error(`${arg} needs a value.`);\n }\n index += 1;\n return value;\n };\n switch (arg) {\n case \"--config\":\n options.configPath = next();\n break;\n case \"--database-url\":\n options.databaseUrl = next();\n break;\n case \"--allow-direct-connection\":\n options.allowDirectConnection = true;\n break;\n case \"--port\": {\n const port = Number(next());\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(\"--port must be an integer between 1 and 65535.\");\n }\n options.port = port;\n break;\n }\n case \"--host\":\n options.host = next();\n break;\n case \"--actions\":\n options.actions = true;\n break;\n case \"--poll\": {\n const poll = Number(next());\n if (!Number.isFinite(poll) || poll < 0) {\n throw new Error(\"--poll must be a non-negative number.\");\n }\n options.pollIntervalMs = poll;\n break;\n }\n case \"--help\":\n case \"-h\":\n options.help = true;\n break;\n default:\n throw new Error(`Unknown argument \"${arg}\". Try --help.`);\n }\n }\n return options;\n}\n\n/** Loopback only. A dev console with no auth must not be reachable off-box. */\nconst LOOPBACK = new Set([\"127.0.0.1\", \"localhost\", \"::1\", \"[::1]\"]);\n\nexport function assertLoopback(host: string): void {\n if (!LOOPBACK.has(host)) {\n throw new Error(\n `--host ${host} is refused. This console ships no authentication, so it binds to loopback only. Put the mounted handler in your application if you need it reachable.`,\n );\n }\n}\n\nasync function loadConfigModule(path: string): Promise<ConsoleDevConfig> {\n const url = pathToFileURL(\n path.startsWith(\"/\") ? path : `${process.cwd()}/${path}`,\n ).href;\n const module: Record<string, unknown> = await import(url);\n const candidate = module.default ?? module.console;\n const resolved =\n typeof candidate === \"function\"\n ? await (\n candidate as () => ConsoleDevConfig | Promise<ConsoleDevConfig>\n )()\n : candidate;\n if (\n typeof resolved !== \"object\" ||\n resolved === null ||\n !(\"reader\" in resolved)\n ) {\n throw new Error(\n `${path} must export { reader, kernel?, dispose? } as its default export (or as \\`console\\`), or a function returning it.`,\n );\n }\n return resolved as ConsoleDevConfig;\n}\n\n/**\n * The escape hatch: our own Prisma client from a connection string.\n *\n * Deliberately behind an explicit flag. `@prisma/client` is imported\n * dynamically so the package does not depend on it for the mounted path,\n * which is the path that matters.\n */\nasync function connectDirectly(databaseUrl: string): Promise<ConsoleDevConfig> {\n const { createPrismaConsoleReadPort } = await import(\"./prisma-read-port\");\n // Loaded through a variable specifier so TypeScript does not try to\n // resolve `@prisma/client` at build time: this package does not depend on\n // it, and this is the one path that touches it.\n const prismaSpecifier = \"@prisma/client\";\n const prismaModule: Record<string, unknown> = await import(\n prismaSpecifier\n ).catch(() => {\n throw new Error(\n \"--database-url needs @prisma/client installed in this project.\",\n );\n });\n const PrismaClient = prismaModule.PrismaClient as\n | (new (\n args: unknown,\n ) => {\n $disconnect(): Promise<void>;\n })\n | undefined;\n if (!PrismaClient) {\n throw new Error(\"@prisma/client did not export PrismaClient.\");\n }\n const client = new PrismaClient({\n datasources: { db: { url: databaseUrl } },\n });\n return {\n reader: createPrismaConsoleReadPort(\n client as unknown as Parameters<typeof createPrismaConsoleReadPort>[0],\n ),\n dispose: () => client.$disconnect(),\n };\n}\n\nexport async function main(argv: string[]): Promise<number> {\n let options: CliOptions;\n try {\n options = parseArgs(argv);\n } catch (error) {\n process.stderr.write(\n `${error instanceof Error ? error.message : String(error)}\\n`,\n );\n return 2;\n }\n\n if (options.help) {\n process.stdout.write(HELP);\n return 0;\n }\n\n try {\n assertLoopback(options.host);\n\n let config: ConsoleDevConfig;\n if (options.configPath) {\n config = await loadConfigModule(options.configPath);\n } else {\n const databaseUrl = options.databaseUrl ?? process.env.DATABASE_URL;\n if (!databaseUrl) {\n process.stderr.write(\n \"Nothing to connect to. Pass --config <path> (preferred: your code builds the client, so row-level security still applies) or --database-url with --allow-direct-connection.\\n\",\n );\n return 2;\n }\n if (!options.allowDirectConnection) {\n process.stderr.write(\n \"Refusing to open a database connection of my own.\\n\\n\" +\n \"A direct connection is a second session and therefore a second security context: row-level security scoped to your application's session will not apply, and this console would read whatever its role can read, across every tenant.\\n\\n\" +\n \"Either pass --config <path> to a module that builds the reader the way your own code does, or re-run with --allow-direct-connection to accept that.\\n\",\n );\n return 2;\n }\n config = await connectDirectly(databaseUrl);\n }\n\n if (options.actions && !config.kernel) {\n process.stderr.write(\n \"--actions needs a kernel. Export one from your --config module; writes go through kernel commands, not SQL.\\n\",\n );\n await config.dispose?.();\n return 2;\n }\n\n const handler = createWorkflowConsole({\n reader: config.reader,\n kernel: config.kernel,\n actions: options.actions,\n pollIntervalMs: options.pollIntervalMs,\n // Local, single-operator, already behind loopback: there is no\n // principal to distinguish, so everything the console offers is\n // allowed. The mounted handler defaults the other way, to deny.\n authorize: () => true,\n });\n\n const server = createServer(toNodeHandler(handler));\n await new Promise<void>((resolve) => {\n server.listen(options.port, options.host, resolve);\n });\n\n process.stdout.write(\n `Workflow console on http://${options.host}:${options.port}/\\n` +\n ` ${options.actions ? \"actions enabled\" : \"read-only\"}, polling every ${options.pollIntervalMs}ms\\n` +\n \" Development tool: loopback only, no authentication. Do not expose it.\\n\",\n );\n\n const shutdown = () => {\n server.close(() => {\n void Promise.resolve(config.dispose?.()).then(() => process.exit(0));\n });\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n return 0;\n } catch (error) {\n process.stderr.write(\n `${error instanceof Error ? error.message : String(error)}\\n`,\n );\n return 1;\n }\n}\n\n// Only run when invoked as a program, so the exported pieces stay testable.\nif (\n process.argv[1] &&\n import.meta.url === pathToFileURL(process.argv[1]).href\n) {\n const code = await main(process.argv.slice(2));\n if (code !== 0) process.exit(code);\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { d as ConsoleAuthorizeContext, C as ConsoleKernel, c as ConsoleActionEvent } from './actions-DOP8plrl.js';
|
|
2
|
+
import { C as ConsoleReadPort } from './read-port-CRmaGAKb.js';
|
|
3
|
+
|
|
4
|
+
interface WorkflowConsoleOptions {
|
|
5
|
+
/** Reads run on this. It is constructed from the caller's client or transaction; the console never opens one. */
|
|
6
|
+
reader: ConsoleReadPort;
|
|
7
|
+
/**
|
|
8
|
+
* Per-request authorisation. Omit it and every action is denied — a
|
|
9
|
+
* console that shipped with a permissive default would be a way to leak
|
|
10
|
+
* a tenant's runs by forgetting one line of configuration.
|
|
11
|
+
*
|
|
12
|
+
* It runs *after* the host's authentication. Assume "is this request
|
|
13
|
+
* authenticated?" is already decided; answer only "may this principal do
|
|
14
|
+
* this?". A throw denies, so a bug in the callback fails closed.
|
|
15
|
+
*/
|
|
16
|
+
authorize?: (context: ConsoleAuthorizeContext) => boolean | Promise<boolean>;
|
|
17
|
+
/** Required only when `actions` is true. */
|
|
18
|
+
kernel?: ConsoleKernel;
|
|
19
|
+
/** Write actions are off unless this is true. Read-only is the default. */
|
|
20
|
+
actions?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Mount path, if the handler cannot work it out. It normally can: the
|
|
23
|
+
* SPA routes on the fragment, so the only paths the server ever sees are
|
|
24
|
+
* the mount root itself, `<root>/api/...` and `<root>/assets/...`.
|
|
25
|
+
*/
|
|
26
|
+
basePath?: string;
|
|
27
|
+
/** UI poll interval in ms. Default 5000; the surveyed consoles sit between 1s and 5s and a shared console should be at the slow end. */
|
|
28
|
+
pollIntervalMs?: number;
|
|
29
|
+
/** Fix the poll interval and hide the picker. */
|
|
30
|
+
forcePollInterval?: boolean;
|
|
31
|
+
/** Supply a CSP nonce for the injected config script, for hosts running a strict policy. */
|
|
32
|
+
cspNonce?: (request: Request) => string | undefined;
|
|
33
|
+
/** Called after every allowed write. The audit trail hosts would otherwise have to build themselves. */
|
|
34
|
+
onAction?: (event: ConsoleActionEvent) => void | Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
type WorkflowConsoleHandler = (request: Request) => Promise<Response>;
|
|
37
|
+
/**
|
|
38
|
+
* Build a fetch handler serving the console's JSON API and its prebuilt UI.
|
|
39
|
+
*
|
|
40
|
+
* It opens no connection: every read goes through `reader`, which the caller
|
|
41
|
+
* built over the client or transaction they already had, so the console runs
|
|
42
|
+
* in the caller's security context and inherits their row-level security
|
|
43
|
+
* rather than working around it.
|
|
44
|
+
*/
|
|
45
|
+
declare function createWorkflowConsole(options: WorkflowConsoleOptions): WorkflowConsoleHandler;
|
|
46
|
+
|
|
47
|
+
export { type WorkflowConsoleHandler as W, type WorkflowConsoleOptions as a, createWorkflowConsole as c };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export { a as CONSOLE_ACTIONS, b as ConsoleAction, c as ConsoleActionEvent, d as ConsoleAuthorizeContext, C as ConsoleKernel, W as WRITE_ACTIONS, i as isWriteAction } from './actions-DOP8plrl.js';
|
|
2
|
+
export { W as WorkflowConsoleHandler, a as WorkflowConsoleOptions, c as createWorkflowConsole } from './handler-Cq2cVh7p.js';
|
|
3
|
+
import { C as ConsoleReadPort, a as ConsoleCapabilities, R as RunListQuery, b as RunListPage, c as RunDetailOptions, d as RunDetail, e as RunEvent, Q as QueueHealth, S as SuspendedStage, D as DeadLetter, W as WorkerInstance, f as CostQuery, g as CostBucket } from './read-port-CRmaGAKb.js';
|
|
4
|
+
export { A as ALL_CAPABILITIES, h as AnnotationSummary, i as CONSOLE_STATUSES, j as ConsoleStatus, k as DEFAULT_RUN_LIMIT, L as LogEntry, M as MAX_LIMIT, l as RunCursor, m as RunListFilters, n as RunSummary, o as StageSummary, p as StepSummary, q as clampLimit, r as decodeCursor, s as encodeCursor, t as isConsoleStatus } from './read-port-CRmaGAKb.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A console query was cancelled by `statement_timeout` (Postgres `57014`).
|
|
8
|
+
*
|
|
9
|
+
* Thrown rather than swallowed so the handler can answer with an honest
|
|
10
|
+
* "that query was too expensive" instead of holding the request open. The
|
|
11
|
+
* UI renders it as an inline banner and leaves the filter bar interactive,
|
|
12
|
+
* so the operator can narrow the filter and retry — a page that keeps
|
|
13
|
+
* working is worth more than a spinner that never resolves.
|
|
14
|
+
*/
|
|
15
|
+
declare class ConsoleQueryTimeoutError extends Error {
|
|
16
|
+
readonly query: string;
|
|
17
|
+
readonly timeoutMs: number;
|
|
18
|
+
readonly code: "query_timeout";
|
|
19
|
+
constructor(query: string, timeoutMs: number, options?: {
|
|
20
|
+
cause?: unknown;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Recognise a cancelled statement through whatever wrapper the driver put
|
|
25
|
+
* around it. Prisma surfaces the SQLSTATE on `meta.code` for
|
|
26
|
+
* `PrismaClientKnownRequestError` P2010, node-postgres puts it on `code`,
|
|
27
|
+
* and a raw error may only carry the message.
|
|
28
|
+
*/
|
|
29
|
+
declare function isStatementTimeout(error: unknown): boolean;
|
|
30
|
+
/** The request asked for something the console cannot serve. */
|
|
31
|
+
declare class ConsoleBadRequestError extends Error {
|
|
32
|
+
readonly code: "bad_request";
|
|
33
|
+
constructor(message: string);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Minimal structural client contract.
|
|
38
|
+
*
|
|
39
|
+
* This avoids a hard dependency on `@prisma/client` or the engine's internals,
|
|
40
|
+
* letting callers pass either their root PrismaClient or an existing interactive
|
|
41
|
+
* transaction client.
|
|
42
|
+
*/
|
|
43
|
+
interface ConsolePrismaClient {
|
|
44
|
+
$queryRawUnsafe<T = unknown>(query: string, ...values: unknown[]): Promise<T>;
|
|
45
|
+
$executeRawUnsafe?(query: string, ...values: unknown[]): Promise<number>;
|
|
46
|
+
$transaction?<T>(fn: (tx: ConsolePrismaClient) => Promise<T>): Promise<T>;
|
|
47
|
+
}
|
|
48
|
+
interface PrismaConsoleReadPortOptions {
|
|
49
|
+
statusEnumName?: string;
|
|
50
|
+
statementTimeoutMs?: number;
|
|
51
|
+
now?: () => Date;
|
|
52
|
+
}
|
|
53
|
+
declare class PrismaConsoleReadPort implements ConsoleReadPort {
|
|
54
|
+
readonly capabilities: ConsoleCapabilities;
|
|
55
|
+
private readonly prisma;
|
|
56
|
+
private readonly statusEnumName;
|
|
57
|
+
private readonly statementTimeoutMs;
|
|
58
|
+
private readonly now;
|
|
59
|
+
constructor(prisma: ConsolePrismaClient, options?: PrismaConsoleReadPortOptions);
|
|
60
|
+
/** One `SELECT` against whichever client the surrounding `read` gave us. */
|
|
61
|
+
private select;
|
|
62
|
+
/**
|
|
63
|
+
* Runs one console read, whatever number of statements it takes, and
|
|
64
|
+
* translates a cancelled statement into `ConsoleQueryTimeoutError`.
|
|
65
|
+
*
|
|
66
|
+
* The callback receives the client to issue statements on, and every read
|
|
67
|
+
* that needs more than one statement issues all of them here, inside a
|
|
68
|
+
* single transaction — a run-detail page is six queries, and giving each
|
|
69
|
+
* its own interactive transaction would take six connections out of the
|
|
70
|
+
* consumer's pool to render one page. One transaction also means the six
|
|
71
|
+
* results are a coherent snapshot rather than six moments.
|
|
72
|
+
*
|
|
73
|
+
* The transaction is opened only when we were handed a root client. When
|
|
74
|
+
* `$transaction` is absent we are already inside the caller's transaction
|
|
75
|
+
* — the row-level-security case this package exists for — and we neither
|
|
76
|
+
* open another nor touch `statement_timeout`: `SET LOCAL` there would
|
|
77
|
+
* silently re-time the remainder of a transaction the console did not
|
|
78
|
+
* open and does not get to reconfigure. Timeouts in that case are the
|
|
79
|
+
* caller's to set, and a cancellation is still reported honestly because
|
|
80
|
+
* the catch below does not care who set the limit.
|
|
81
|
+
*/
|
|
82
|
+
private read;
|
|
83
|
+
/** Convenience for the reads that are a single statement. */
|
|
84
|
+
private query;
|
|
85
|
+
listRuns(query: RunListQuery): Promise<RunListPage>;
|
|
86
|
+
getRunDetail(runId: string, options?: RunDetailOptions): Promise<RunDetail | null>;
|
|
87
|
+
listRunEvents(runId: string, afterSequence: number, limit?: number): Promise<RunEvent[]>;
|
|
88
|
+
getQueueHealth(): Promise<QueueHealth>;
|
|
89
|
+
listSuspendedStages(limit?: number): Promise<SuspendedStage[]>;
|
|
90
|
+
listDeadLetters(limit?: number): Promise<DeadLetter[]>;
|
|
91
|
+
listWorkers(): Promise<WorkerInstance[]>;
|
|
92
|
+
getCosts(query: CostQuery): Promise<CostBucket[]>;
|
|
93
|
+
}
|
|
94
|
+
declare function createPrismaConsoleReadPort(prisma: ConsolePrismaClient, options?: PrismaConsoleReadPortOptions): ConsoleReadPort;
|
|
95
|
+
|
|
96
|
+
export { ConsoleBadRequestError, ConsoleCapabilities, type ConsolePrismaClient, ConsoleQueryTimeoutError, ConsoleReadPort, CostBucket, CostQuery, DeadLetter, PrismaConsoleReadPort, type PrismaConsoleReadPortOptions, QueueHealth, RunDetail, RunDetailOptions, RunEvent, RunListPage, RunListQuery, SuspendedStage, WorkerInstance, createPrismaConsoleReadPort, isStatementTimeout };
|