workplane 0.1.1

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/server.js ADDED
@@ -0,0 +1,851 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../server/src/index.ts
4
+ import { createServer } from "http";
5
+ import { URL } from "url";
6
+ import { DBOS as DBOS2 } from "@dbos-inc/dbos-sdk";
7
+ import { ZodError } from "zod";
8
+
9
+ // ../core/src/config.ts
10
+ import { z } from "zod";
11
+
12
+ // ../core/src/env.ts
13
+ import { config as dotenvConfig } from "dotenv";
14
+ var envLoaded = false;
15
+ function loadLocalEnv() {
16
+ if (envLoaded) {
17
+ return;
18
+ }
19
+ dotenvConfig({ path: ".env" });
20
+ dotenvConfig({ path: ".env.local", override: true });
21
+ envLoaded = true;
22
+ }
23
+
24
+ // ../core/src/config.ts
25
+ loadLocalEnv();
26
+ var serverEnvSchema = z.object({
27
+ DATABASE_URL: z.string().min(1),
28
+ WORKPLANE_SERVER_PORT: z.string().optional().default("8787").transform((value) => Number.parseInt(value, 10)),
29
+ WORKPLANE_NODE_TOKEN: z.string().optional(),
30
+ WORKPLANE_OPERATOR_TOKEN: z.string().optional(),
31
+ WORKPLANE_ENV_ALLOWLIST: z.string().optional()
32
+ });
33
+ var nodeEnvSchema = z.object({
34
+ WORKPLANE_SERVER_URL: z.string().url().optional().default("http://localhost:8787"),
35
+ WORKPLANE_NODE_NAME: z.string().optional().default("local-node-1"),
36
+ WORKPLANE_NODE_ID: z.string().optional(),
37
+ WORKPLANE_NODE_CAPABILITIES: z.string().optional().default("shell,git,node,typescript,aider,ollama,codex,claude-code"),
38
+ WORKPLANE_NODE_TOKEN: z.string().optional(),
39
+ WORKPLANE_POLL_INTERVAL_MS: z.string().optional().default("3000").transform((value) => Number.parseInt(value, 10)),
40
+ WORKPLANE_ENV_ALLOWLIST: z.string().optional()
41
+ });
42
+ function loadServerConfig(env = process.env) {
43
+ const parsed = serverEnvSchema.parse(env);
44
+ return {
45
+ databaseUrl: parsed.DATABASE_URL,
46
+ port: parsed.WORKPLANE_SERVER_PORT,
47
+ nodeToken: parsed.WORKPLANE_NODE_TOKEN,
48
+ operatorToken: parsed.WORKPLANE_OPERATOR_TOKEN,
49
+ envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? "")
50
+ };
51
+ }
52
+ function parseCsv(value) {
53
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
54
+ }
55
+
56
+ // ../db/src/client.ts
57
+ import { Pool } from "pg";
58
+ var pools = /* @__PURE__ */ new Map();
59
+ function getPool(databaseUrl) {
60
+ const existing = pools.get(databaseUrl);
61
+ if (existing) {
62
+ return existing;
63
+ }
64
+ const pool = new Pool({ connectionString: databaseUrl });
65
+ pools.set(databaseUrl, pool);
66
+ return pool;
67
+ }
68
+
69
+ // ../core/src/ids.ts
70
+ import { randomUUID } from "crypto";
71
+ function makeId(prefix) {
72
+ return `${prefix}_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
73
+ }
74
+
75
+ // ../server/src/store.ts
76
+ function mapTask(row) {
77
+ return {
78
+ id: String(row.id),
79
+ kind: String(row.kind),
80
+ adapter: String(row.adapter),
81
+ payload: row.payload ?? {},
82
+ requires: row.requires ?? [],
83
+ status: row.status,
84
+ createdAt: new Date(String(row.created_at)).toISOString(),
85
+ updatedAt: new Date(String(row.updated_at)).toISOString()
86
+ };
87
+ }
88
+ function mapRun(row) {
89
+ return {
90
+ id: String(row.id),
91
+ taskId: String(row.task_id),
92
+ nodeId: String(row.node_id),
93
+ attempt: Number(row.attempt),
94
+ status: row.status,
95
+ startedAt: row.started_at ? new Date(String(row.started_at)).toISOString() : null,
96
+ endedAt: row.ended_at ? new Date(String(row.ended_at)).toISOString() : null,
97
+ error: row.error ? String(row.error) : null
98
+ };
99
+ }
100
+ function mapArtifact(row) {
101
+ return {
102
+ id: String(row.id),
103
+ runId: String(row.run_id),
104
+ type: String(row.type),
105
+ name: String(row.name),
106
+ path: String(row.path),
107
+ metadata: row.metadata ?? null,
108
+ createdAt: new Date(String(row.created_at)).toISOString()
109
+ };
110
+ }
111
+ var PgStore = class {
112
+ constructor(pool) {
113
+ this.pool = pool;
114
+ }
115
+ pool;
116
+ async createTask(input) {
117
+ const taskId = makeId("task");
118
+ const result = await this.pool.query(
119
+ `
120
+ insert into tasks (id, kind, adapter, payload, requires, status)
121
+ values ($1, $2, $3, $4::jsonb, $5::text[], 'queued')
122
+ returning *
123
+ `,
124
+ [taskId, input.kind, input.adapter, JSON.stringify(input.payload), input.requires ?? []]
125
+ );
126
+ return mapTask(result.rows[0]);
127
+ }
128
+ async listTasks(status) {
129
+ const result = status ? await this.pool.query("select * from tasks where status = $1 order by created_at desc limit 100", [status]) : await this.pool.query("select * from tasks order by created_at desc limit 100");
130
+ return result.rows.map((row) => mapTask(row));
131
+ }
132
+ async getTask(taskId) {
133
+ const result = await this.pool.query("select * from tasks where id = $1", [taskId]);
134
+ if (result.rowCount === 0) {
135
+ return null;
136
+ }
137
+ return mapTask(result.rows[0]);
138
+ }
139
+ async listRuns(filters) {
140
+ const taskId = filters?.taskId;
141
+ const status = filters?.status;
142
+ const result = taskId && status ? await this.pool.query("select * from runs where task_id = $1 and status = $2 order by created_at desc", [
143
+ taskId,
144
+ status
145
+ ]) : taskId ? await this.pool.query("select * from runs where task_id = $1 order by created_at desc", [taskId]) : status ? await this.pool.query("select * from runs where status = $1 order by created_at desc limit 100", [status]) : await this.pool.query("select * from runs order by created_at desc limit 100");
146
+ return result.rows.map((row) => mapRun(row));
147
+ }
148
+ async getRun(runId) {
149
+ const result = await this.pool.query("select * from runs where id = $1", [runId]);
150
+ if (result.rowCount === 0) {
151
+ return null;
152
+ }
153
+ return mapRun(result.rows[0]);
154
+ }
155
+ async registerNode(name, capabilities, preferredId) {
156
+ const existing = await this.pool.query("select * from nodes where name = $1", [name]);
157
+ if (existing.rowCount && existing.rowCount > 0) {
158
+ const row = existing.rows[0];
159
+ const nodeId2 = String(row.id);
160
+ const updated = await this.pool.query(
161
+ `
162
+ update nodes
163
+ set capabilities = $2::text[], status = 'online', last_heartbeat_at = now(), updated_at = now()
164
+ where id = $1
165
+ returning *
166
+ `,
167
+ [nodeId2, capabilities]
168
+ );
169
+ return this.mapNodeRecord(updated.rows[0]);
170
+ }
171
+ const nodeId = preferredId ?? makeId("node");
172
+ const result = await this.pool.query(
173
+ `
174
+ insert into nodes (id, name, capabilities, status, last_heartbeat_at)
175
+ values ($1, $2, $3::text[], 'online', now())
176
+ returning *
177
+ `,
178
+ [nodeId, name, capabilities]
179
+ );
180
+ return this.mapNodeRecord(result.rows[0]);
181
+ }
182
+ async getRunCancellationState(runId) {
183
+ const result = await this.pool.query(
184
+ `
185
+ select r.status as run_status, t.status as task_status
186
+ from runs r
187
+ join tasks t on t.id = r.task_id
188
+ where r.id = $1
189
+ `,
190
+ [runId]
191
+ );
192
+ if (result.rowCount === 0) {
193
+ return null;
194
+ }
195
+ const row = result.rows[0];
196
+ return {
197
+ runStatus: String(row.run_status),
198
+ taskStatus: String(row.task_status)
199
+ };
200
+ }
201
+ mapNodeRecord(row) {
202
+ return {
203
+ id: String(row.id),
204
+ name: String(row.name),
205
+ capabilities: row.capabilities ?? [],
206
+ status: row.status,
207
+ lastHeartbeatAt: row.last_heartbeat_at ? new Date(String(row.last_heartbeat_at)).toISOString() : null
208
+ };
209
+ }
210
+ async pollNode(nodeId, capabilities) {
211
+ const client = await this.pool.connect();
212
+ try {
213
+ await client.query("begin");
214
+ await this.touchNode(client, nodeId, capabilities);
215
+ const taskResult = await client.query(
216
+ `
217
+ select *
218
+ from tasks
219
+ where status = 'queued'
220
+ and requires <@ $1::text[]
221
+ order by created_at asc
222
+ limit 1
223
+ for update skip locked
224
+ `,
225
+ [capabilities]
226
+ );
227
+ if (taskResult.rowCount === 0) {
228
+ await client.query("commit");
229
+ return null;
230
+ }
231
+ const taskRow = taskResult.rows[0];
232
+ const taskId = String(taskRow.id);
233
+ const runId = makeId("run");
234
+ const attemptResult = await client.query("select coalesce(max(attempt), 0) as max_attempt from runs where task_id = $1", [
235
+ taskId
236
+ ]);
237
+ const attempt = Number(attemptResult.rows[0].max_attempt) + 1;
238
+ const runResult = await client.query(
239
+ `
240
+ insert into runs (id, task_id, node_id, attempt, status, started_at)
241
+ values ($1, $2, $3, $4, 'assigned', now())
242
+ returning *
243
+ `,
244
+ [runId, taskId, nodeId, attempt]
245
+ );
246
+ await client.query("update tasks set status = 'assigned', updated_at = now() where id = $1", [taskId]);
247
+ await client.query("commit");
248
+ return {
249
+ task: mapTask(taskRow),
250
+ run: mapRun(runResult.rows[0])
251
+ };
252
+ } catch (error) {
253
+ await client.query("rollback");
254
+ throw error;
255
+ } finally {
256
+ client.release();
257
+ }
258
+ }
259
+ async updateRunStatus(runId, status, error) {
260
+ const client = await this.pool.connect();
261
+ try {
262
+ await client.query("begin");
263
+ const currentResult = await client.query(
264
+ `
265
+ select r.*, t.status as task_status
266
+ from runs r
267
+ join tasks t on t.id = r.task_id
268
+ where r.id = $1
269
+ for update
270
+ `,
271
+ [runId]
272
+ );
273
+ if (currentResult.rowCount === 0) {
274
+ await client.query("rollback");
275
+ return null;
276
+ }
277
+ const currentRow = currentResult.rows[0];
278
+ const currentTaskStatus = String(currentRow.task_status);
279
+ if (currentTaskStatus === "cancelled" && status !== "cancelled") {
280
+ await client.query("commit");
281
+ return mapRun(currentRow);
282
+ }
283
+ const runResult = await client.query(
284
+ `
285
+ update runs
286
+ set status = $2, error = $3, ended_at = case when $2 in ('succeeded', 'failed', 'cancelled') then now() else ended_at end, updated_at = now()
287
+ where id = $1
288
+ returning *
289
+ `,
290
+ [runId, status, error ?? null]
291
+ );
292
+ if (runResult.rowCount === 0) {
293
+ await client.query("rollback");
294
+ return null;
295
+ }
296
+ const run = mapRun(runResult.rows[0]);
297
+ const nextTaskStatus = status === "succeeded" || status === "failed" || status === "cancelled" ? status : "running";
298
+ if (currentTaskStatus !== "cancelled" || status === "cancelled") {
299
+ await client.query("update tasks set status = $2, updated_at = now() where id = $1", [run.taskId, nextTaskStatus]);
300
+ }
301
+ await client.query("commit");
302
+ return run;
303
+ } catch (error2) {
304
+ await client.query("rollback");
305
+ throw error2;
306
+ } finally {
307
+ client.release();
308
+ }
309
+ }
310
+ async appendRunLogs(runId, logs) {
311
+ let inserted = 0;
312
+ for (const log of logs) {
313
+ await this.pool.query(
314
+ `
315
+ insert into run_logs (run_id, step_name, stream, message)
316
+ values ($1, $2, $3, $4)
317
+ `,
318
+ [runId, log.stepName ?? null, log.stream, log.message]
319
+ );
320
+ inserted += 1;
321
+ }
322
+ return inserted;
323
+ }
324
+ async getRunLogs(runId) {
325
+ const result = await this.pool.query("select * from run_logs where run_id = $1 order by timestamp asc", [runId]);
326
+ return result.rows;
327
+ }
328
+ async createArtifact(runId, input) {
329
+ const artifactId = makeId("artifact");
330
+ const result = await this.pool.query(
331
+ `
332
+ insert into artifacts (id, run_id, type, name, path, metadata)
333
+ values ($1, $2, $3, $4, $5, $6::jsonb)
334
+ returning *
335
+ `,
336
+ [artifactId, runId, input.type, input.name, input.path, input.metadata ? JSON.stringify(input.metadata) : null]
337
+ );
338
+ return mapArtifact(result.rows[0]);
339
+ }
340
+ async listRunArtifacts(runId) {
341
+ const result = await this.pool.query("select * from artifacts where run_id = $1 order by created_at asc", [runId]);
342
+ return result.rows.map((row) => mapArtifact(row));
343
+ }
344
+ async retryTask(taskId) {
345
+ const result = await this.pool.query(
346
+ `
347
+ update tasks
348
+ set status = 'queued', updated_at = now()
349
+ where id = $1 and status = 'failed'
350
+ returning *
351
+ `,
352
+ [taskId]
353
+ );
354
+ if (result.rowCount === 0) {
355
+ return null;
356
+ }
357
+ return mapTask(result.rows[0]);
358
+ }
359
+ async cancelTask(taskId) {
360
+ const client = await this.pool.connect();
361
+ try {
362
+ await client.query("begin");
363
+ const taskResult = await client.query("select * from tasks where id = $1 for update", [taskId]);
364
+ if (taskResult.rowCount === 0) {
365
+ await client.query("rollback");
366
+ return null;
367
+ }
368
+ const taskRow = taskResult.rows[0];
369
+ const taskStatus = String(taskRow.status);
370
+ if (taskStatus === "succeeded" || taskStatus === "failed" || taskStatus === "cancelled") {
371
+ await client.query("rollback");
372
+ return null;
373
+ }
374
+ if (taskStatus === "assigned" || taskStatus === "running") {
375
+ const runResult = await client.query(
376
+ `
377
+ select * from runs
378
+ where task_id = $1
379
+ order by attempt desc
380
+ limit 1
381
+ for update
382
+ `,
383
+ [taskId]
384
+ );
385
+ if (runResult.rowCount !== 0) {
386
+ const runStatus = String(runResult.rows[0].status);
387
+ if (runStatus !== "succeeded" && runStatus !== "failed" && runStatus !== "cancelled") {
388
+ await client.query(
389
+ `
390
+ update runs
391
+ set status = 'cancelled', ended_at = now(), updated_at = now(), error = coalesce(error, 'task cancelled')
392
+ where id = $1
393
+ `,
394
+ [runResult.rows[0].id]
395
+ );
396
+ }
397
+ }
398
+ }
399
+ const updateTaskResult = await client.query(
400
+ `
401
+ update tasks
402
+ set status = 'cancelled', updated_at = now()
403
+ where id = $1
404
+ returning *
405
+ `,
406
+ [taskId]
407
+ );
408
+ await client.query("commit");
409
+ return mapTask(updateTaskResult.rows[0]);
410
+ } catch (error) {
411
+ await client.query("rollback");
412
+ throw error;
413
+ } finally {
414
+ client.release();
415
+ }
416
+ }
417
+ async touchNode(client, nodeId, capabilities) {
418
+ await client.query(
419
+ `
420
+ update nodes
421
+ set capabilities = $2::text[], status = 'online', last_heartbeat_at = now(), updated_at = now()
422
+ where id = $1
423
+ `,
424
+ [nodeId, capabilities]
425
+ );
426
+ }
427
+ };
428
+
429
+ // ../server/src/workflows.ts
430
+ import { DBOS } from "@dbos-inc/dbos-sdk";
431
+ function registerWorkflows(store) {
432
+ const createTask = DBOS.registerWorkflow(
433
+ async (input) => DBOS.runStep(() => store.createTask(input), {
434
+ name: "store-create-task"
435
+ }),
436
+ { name: "createTaskWorkflow" }
437
+ );
438
+ const retryTask = DBOS.registerWorkflow(
439
+ async (taskId) => DBOS.runStep(() => store.retryTask(taskId), {
440
+ name: "store-retry-task"
441
+ }),
442
+ { name: "retryTaskWorkflow" }
443
+ );
444
+ const cancelTask = DBOS.registerWorkflow(
445
+ async (taskId) => DBOS.runStep(() => store.cancelTask(taskId), {
446
+ name: "store-cancel-task"
447
+ }),
448
+ { name: "cancelTaskWorkflow" }
449
+ );
450
+ const updateRunStatus = DBOS.registerWorkflow(
451
+ async (runId, status, error) => DBOS.runStep(() => store.updateRunStatus(runId, status, error), {
452
+ name: "store-update-run-status"
453
+ }),
454
+ { name: "updateRunStatusWorkflow" }
455
+ );
456
+ const appendRunLogs = DBOS.registerWorkflow(
457
+ async (runId, logs) => DBOS.runStep(() => store.appendRunLogs(runId, logs), {
458
+ name: "store-append-run-logs"
459
+ }),
460
+ { name: "appendRunLogsWorkflow" }
461
+ );
462
+ const createArtifact = DBOS.registerWorkflow(
463
+ async (runId, input) => DBOS.runStep(() => store.createArtifact(runId, input), {
464
+ name: "store-create-artifact"
465
+ }),
466
+ { name: "createArtifactWorkflow" }
467
+ );
468
+ return {
469
+ createTask,
470
+ retryTask,
471
+ cancelTask,
472
+ updateRunStatus,
473
+ appendRunLogs,
474
+ createArtifact
475
+ };
476
+ }
477
+
478
+ // ../server/src/validation.ts
479
+ import { z as z2 } from "zod";
480
+ var repoFields = {
481
+ repo: z2.string().min(1).optional(),
482
+ branch: z2.string().min(1).optional()
483
+ };
484
+ var shellTaskSchema = z2.object({
485
+ kind: z2.literal("shell.exec"),
486
+ adapter: z2.literal("shell"),
487
+ requires: z2.array(z2.string().min(1)).optional(),
488
+ payload: z2.object({
489
+ command: z2.string().min(1),
490
+ args: z2.array(z2.string()).optional(),
491
+ cwd: z2.string().min(1).optional(),
492
+ ...repoFields
493
+ })
494
+ });
495
+ var aiderTaskSchema = z2.object({
496
+ kind: z2.literal("agent.run"),
497
+ adapter: z2.literal("aider"),
498
+ requires: z2.array(z2.string().min(1)).optional(),
499
+ payload: z2.object({
500
+ prompt: z2.string().min(1),
501
+ model: z2.string().min(1).optional(),
502
+ repo: z2.string().min(1),
503
+ branch: z2.string().min(1).optional(),
504
+ testCommand: z2.string().min(1).optional()
505
+ })
506
+ });
507
+ var ollamaTaskSchema = z2.object({
508
+ kind: z2.literal("inference.batch"),
509
+ adapter: z2.literal("ollama"),
510
+ requires: z2.array(z2.string().min(1)).optional(),
511
+ payload: z2.object({
512
+ model: z2.string().min(1),
513
+ prompt: z2.string().min(1),
514
+ inputFile: z2.string().min(1).optional()
515
+ })
516
+ });
517
+ var harnessPayloadSchema = z2.object({
518
+ prompt: z2.string().min(1),
519
+ model: z2.string().min(1).optional(),
520
+ repo: z2.string().min(1),
521
+ branch: z2.string().min(1).optional(),
522
+ testCommand: z2.string().min(1).optional(),
523
+ extraArgs: z2.array(z2.string()).optional()
524
+ });
525
+ var codexTaskSchema = z2.object({
526
+ kind: z2.literal("agent.run"),
527
+ adapter: z2.literal("codex"),
528
+ requires: z2.array(z2.string().min(1)).optional(),
529
+ payload: harnessPayloadSchema
530
+ });
531
+ var claudeCodeTaskSchema = z2.object({
532
+ kind: z2.literal("agent.run"),
533
+ adapter: z2.literal("claude-code"),
534
+ requires: z2.array(z2.string().min(1)).optional(),
535
+ payload: harnessPayloadSchema
536
+ });
537
+ var taskSchema = z2.discriminatedUnion("adapter", [
538
+ shellTaskSchema,
539
+ aiderTaskSchema,
540
+ ollamaTaskSchema,
541
+ codexTaskSchema,
542
+ claudeCodeTaskSchema
543
+ ]);
544
+ function validateCreateTaskInput(input) {
545
+ return taskSchema.parse(input);
546
+ }
547
+
548
+ // ../core/src/auth.ts
549
+ function parseBearerToken(authorizationHeader) {
550
+ if (!authorizationHeader) {
551
+ return void 0;
552
+ }
553
+ const match = /^Bearer\s+(.+)$/i.exec(authorizationHeader.trim());
554
+ return match?.[1];
555
+ }
556
+ function isAuthorizedBearer(provided, expected) {
557
+ if (!expected) {
558
+ return true;
559
+ }
560
+ if (!provided) {
561
+ return false;
562
+ }
563
+ return provided === expected;
564
+ }
565
+ function getRequestBearer(req) {
566
+ const header = req.headers.authorization;
567
+ const value = Array.isArray(header) ? header[0] : header;
568
+ return parseBearerToken(value);
569
+ }
570
+
571
+ // ../server/src/auth-middleware.ts
572
+ function checkRouteAuth(req, res, auth, config) {
573
+ const bearer = getRequestBearer(req);
574
+ if (auth === "public") {
575
+ return true;
576
+ }
577
+ if (auth === "node") {
578
+ if (!isAuthorizedBearer(bearer, config.nodeToken)) {
579
+ res.statusCode = 401;
580
+ res.setHeader("content-type", "application/json");
581
+ res.end(JSON.stringify({ error: "unauthorized node request" }));
582
+ return false;
583
+ }
584
+ return true;
585
+ }
586
+ if (auth === "operator") {
587
+ if (!isAuthorizedBearer(bearer, config.operatorToken)) {
588
+ res.statusCode = 401;
589
+ res.setHeader("content-type", "application/json");
590
+ res.end(JSON.stringify({ error: "unauthorized operator request" }));
591
+ return false;
592
+ }
593
+ return true;
594
+ }
595
+ return true;
596
+ }
597
+ function routeAuthFor(method, pathname) {
598
+ if (pathname === "/healthz") {
599
+ return "public";
600
+ }
601
+ if (method === "POST" && pathname === "/nodes/register") {
602
+ return "node";
603
+ }
604
+ if (method === "POST" && /^\/nodes\/[^/]+\/poll$/.test(pathname)) {
605
+ return "node";
606
+ }
607
+ if (method === "POST" && /^\/runs\/[^/]+\/status$/.test(pathname)) {
608
+ return "node";
609
+ }
610
+ if (method === "POST" && /^\/runs\/[^/]+\/logs$/.test(pathname)) {
611
+ return "node";
612
+ }
613
+ if (method === "POST" && /^\/runs\/[^/]+\/artifacts$/.test(pathname)) {
614
+ return "node";
615
+ }
616
+ if (method === "POST" && pathname === "/tasks") {
617
+ return "operator";
618
+ }
619
+ if (method === "POST" && /^\/tasks\/[^/]+\/retry$/.test(pathname)) {
620
+ return "operator";
621
+ }
622
+ if (method === "POST" && /^\/tasks\/[^/]+\/cancel$/.test(pathname)) {
623
+ return "operator";
624
+ }
625
+ return "public";
626
+ }
627
+ function requiresConfiguredToken(auth, config) {
628
+ if (auth === "node") {
629
+ return Boolean(config.nodeToken);
630
+ }
631
+ if (auth === "operator") {
632
+ return Boolean(config.operatorToken);
633
+ }
634
+ return false;
635
+ }
636
+
637
+ // ../server/src/index.ts
638
+ var taskStatuses = /* @__PURE__ */ new Set(["queued", "assigned", "running", "succeeded", "failed", "cancelled"]);
639
+ function parseStatus(value) {
640
+ if (!value) {
641
+ return void 0;
642
+ }
643
+ if (!taskStatuses.has(value)) {
644
+ throw new Error(`invalid status filter: ${value}`);
645
+ }
646
+ return value;
647
+ }
648
+ async function readJson(req) {
649
+ const chunks = [];
650
+ for await (const chunk of req) {
651
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
652
+ }
653
+ const body = Buffer.concat(chunks).toString("utf8");
654
+ return body ? JSON.parse(body) : {};
655
+ }
656
+ function writeJson(res, statusCode, payload) {
657
+ const json = JSON.stringify(payload);
658
+ res.statusCode = statusCode;
659
+ res.setHeader("content-type", "application/json");
660
+ res.end(json);
661
+ }
662
+ async function main() {
663
+ const config = loadServerConfig();
664
+ const pool = getPool(config.databaseUrl);
665
+ const store = new PgStore(pool);
666
+ const workflows = registerWorkflows(store);
667
+ const appName = process.env.DBOS_APPLICATION_NAME ?? "workplane-server";
668
+ const conductorKey = process.env.DBOS_CONDUCTOR_KEY;
669
+ const conductorUrl = process.env.DBOS_CONDUCTOR_URL;
670
+ DBOS2.setConfig({
671
+ name: appName,
672
+ systemDatabaseUrl: config.databaseUrl
673
+ });
674
+ await DBOS2.launch({
675
+ conductorKey,
676
+ conductorURL: conductorUrl
677
+ });
678
+ const server = createServer(async (req, res) => {
679
+ try {
680
+ if (!req.url || !req.method) {
681
+ writeJson(res, 400, { error: "invalid request" });
682
+ return;
683
+ }
684
+ const url = new URL(req.url, "http://localhost");
685
+ const routeAuth = routeAuthFor(req.method, url.pathname);
686
+ const authConfig = { nodeToken: config.nodeToken, operatorToken: config.operatorToken };
687
+ if (requiresConfiguredToken(routeAuth, authConfig) && !checkRouteAuth(req, res, routeAuth, authConfig)) {
688
+ return;
689
+ }
690
+ if (req.method === "GET" && url.pathname === "/healthz") {
691
+ writeJson(res, 200, { ok: true });
692
+ return;
693
+ }
694
+ if (req.method === "POST" && url.pathname === "/tasks") {
695
+ const body = await readJson(req);
696
+ const validated = validateCreateTaskInput(body);
697
+ const handle = await DBOS2.startWorkflow(workflows.createTask)({
698
+ kind: validated.kind,
699
+ adapter: validated.adapter,
700
+ payload: validated.payload,
701
+ requires: validated.requires ?? []
702
+ });
703
+ const task = await handle.getResult();
704
+ writeJson(res, 201, task);
705
+ return;
706
+ }
707
+ if (req.method === "GET" && url.pathname === "/tasks") {
708
+ const status = parseStatus(url.searchParams.get("status"));
709
+ const tasks = await store.listTasks(status);
710
+ writeJson(res, 200, { tasks });
711
+ return;
712
+ }
713
+ if (req.method === "GET" && /^\/tasks\/[^/]+$/.test(url.pathname)) {
714
+ const taskId = url.pathname.split("/")[2];
715
+ const task = await store.getTask(taskId);
716
+ if (!task) {
717
+ writeJson(res, 404, { error: "task not found" });
718
+ return;
719
+ }
720
+ writeJson(res, 200, task);
721
+ return;
722
+ }
723
+ if (req.method === "POST" && /^\/tasks\/[^/]+\/retry$/.test(url.pathname)) {
724
+ const taskId = url.pathname.split("/")[2];
725
+ const handle = await DBOS2.startWorkflow(workflows.retryTask)(taskId);
726
+ const task = await handle.getResult();
727
+ if (!task) {
728
+ writeJson(res, 409, { error: "task is not retryable" });
729
+ return;
730
+ }
731
+ writeJson(res, 200, task);
732
+ return;
733
+ }
734
+ if (req.method === "POST" && /^\/tasks\/[^/]+\/cancel$/.test(url.pathname)) {
735
+ const taskId = url.pathname.split("/")[2];
736
+ const handle = await DBOS2.startWorkflow(workflows.cancelTask)(taskId);
737
+ const task = await handle.getResult();
738
+ if (!task) {
739
+ writeJson(res, 409, { error: "task is not cancellable" });
740
+ return;
741
+ }
742
+ writeJson(res, 200, task);
743
+ return;
744
+ }
745
+ if (req.method === "GET" && url.pathname === "/runs") {
746
+ const taskId = url.searchParams.get("taskId") ?? void 0;
747
+ const status = parseStatus(url.searchParams.get("status"));
748
+ const runs = await store.listRuns({ taskId, status });
749
+ writeJson(res, 200, { runs });
750
+ return;
751
+ }
752
+ if (req.method === "GET" && /^\/runs\/[^/]+$/.test(url.pathname)) {
753
+ const runId = url.pathname.split("/")[2];
754
+ const run = await store.getRun(runId);
755
+ if (!run) {
756
+ writeJson(res, 404, { error: "run not found" });
757
+ return;
758
+ }
759
+ writeJson(res, 200, run);
760
+ return;
761
+ }
762
+ if (req.method === "GET" && /^\/runs\/[^/]+\/logs$/.test(url.pathname)) {
763
+ const runId = url.pathname.split("/")[2];
764
+ const logs = await store.getRunLogs(runId);
765
+ writeJson(res, 200, { logs });
766
+ return;
767
+ }
768
+ if (req.method === "GET" && /^\/runs\/[^/]+\/artifacts$/.test(url.pathname)) {
769
+ const runId = url.pathname.split("/")[2];
770
+ const artifacts = await store.listRunArtifacts(runId);
771
+ writeJson(res, 200, { artifacts });
772
+ return;
773
+ }
774
+ if (req.method === "POST" && url.pathname === "/nodes/register") {
775
+ const body = await readJson(req);
776
+ const node = await store.registerNode(body.name, body.capabilities ?? [], body.nodeId);
777
+ writeJson(res, 201, node);
778
+ return;
779
+ }
780
+ if (req.method === "POST" && /^\/nodes\/[^/]+\/poll$/.test(url.pathname)) {
781
+ const nodeId = url.pathname.split("/")[2];
782
+ const body = await readJson(req);
783
+ const assignment = await store.pollNode(nodeId, body.capabilities ?? []);
784
+ writeJson(res, 200, { assignment });
785
+ return;
786
+ }
787
+ if (req.method === "POST" && /^\/runs\/[^/]+\/status$/.test(url.pathname)) {
788
+ const runId = url.pathname.split("/")[2];
789
+ const body = await readJson(req);
790
+ const handle = await DBOS2.startWorkflow(workflows.updateRunStatus)(runId, body.status, body.error);
791
+ const run = await handle.getResult();
792
+ if (!run) {
793
+ writeJson(res, 404, { error: "run not found" });
794
+ return;
795
+ }
796
+ writeJson(res, 200, run);
797
+ return;
798
+ }
799
+ if (req.method === "POST" && /^\/runs\/[^/]+\/logs$/.test(url.pathname)) {
800
+ const runId = url.pathname.split("/")[2];
801
+ const body = await readJson(req);
802
+ const handle = await DBOS2.startWorkflow(workflows.appendRunLogs)(runId, body.logs ?? []);
803
+ const inserted = await handle.getResult();
804
+ writeJson(res, 200, { inserted });
805
+ return;
806
+ }
807
+ if (req.method === "POST" && /^\/runs\/[^/]+\/artifacts$/.test(url.pathname)) {
808
+ const runId = url.pathname.split("/")[2];
809
+ const body = await readJson(req);
810
+ const handle = await DBOS2.startWorkflow(workflows.createArtifact)(runId, body);
811
+ const artifact = await handle.getResult();
812
+ writeJson(res, 201, artifact);
813
+ return;
814
+ }
815
+ writeJson(res, 404, { error: "not found" });
816
+ } catch (error) {
817
+ if (error instanceof ZodError) {
818
+ writeJson(res, 400, { error: "invalid task payload", details: error });
819
+ return;
820
+ }
821
+ if (error instanceof Error && error.message.startsWith("invalid status filter:")) {
822
+ writeJson(res, 400, { error: error.message });
823
+ return;
824
+ }
825
+ const message = error instanceof Error ? error.message : "internal error";
826
+ writeJson(res, 500, { error: message });
827
+ }
828
+ });
829
+ server.listen(config.port, () => {
830
+ process.stdout.write(`workplane server listening on http://localhost:${config.port}
831
+ `);
832
+ });
833
+ const shutdown = async () => {
834
+ await DBOS2.shutdown();
835
+ await pool.end();
836
+ server.close();
837
+ process.exit(0);
838
+ };
839
+ process.on("SIGINT", () => {
840
+ void shutdown();
841
+ });
842
+ process.on("SIGTERM", () => {
843
+ void shutdown();
844
+ });
845
+ }
846
+ main().catch((error) => {
847
+ process.stderr.write(`${String(error)}
848
+ `);
849
+ process.exit(1);
850
+ });
851
+ //# sourceMappingURL=server.js.map