@umudik/task-bridge 0.0.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/apps/backend/dist/config.js +26 -0
  4. package/apps/backend/dist/db/epic-workflow-db.js +125 -0
  5. package/apps/backend/dist/db/library-db.js +123 -0
  6. package/apps/backend/dist/db/projects-db.js +110 -0
  7. package/apps/backend/dist/db/tasks-db.js +282 -0
  8. package/apps/backend/dist/db/users-db.js +117 -0
  9. package/apps/backend/dist/db/workflow-db.js +186 -0
  10. package/apps/backend/dist/db/workflow-template-db.js +715 -0
  11. package/apps/backend/dist/domain/project-member.js +15 -0
  12. package/apps/backend/dist/domain/task-template-graph.js +63 -0
  13. package/apps/backend/dist/domain/task.js +93 -0
  14. package/apps/backend/dist/domain/work-status.js +30 -0
  15. package/apps/backend/dist/domain/workflow-stage.js +186 -0
  16. package/apps/backend/dist/domain/workflow-state.js +73 -0
  17. package/apps/backend/dist/domain/workflow-template-id.js +6 -0
  18. package/apps/backend/dist/errors/app-error.js +24 -0
  19. package/apps/backend/dist/index.js +67 -0
  20. package/apps/backend/dist/lib/bridge-project.js +24 -0
  21. package/apps/backend/dist/lib/inbox-cursor.js +34 -0
  22. package/apps/backend/dist/lib/strings.js +15 -0
  23. package/apps/backend/dist/logger.js +29 -0
  24. package/apps/backend/dist/mappers/task-response.js +261 -0
  25. package/apps/backend/dist/middleware/auth.js +29 -0
  26. package/apps/backend/dist/openapi.js +716 -0
  27. package/apps/backend/dist/routes/admin-users.js +79 -0
  28. package/apps/backend/dist/routes/auth.js +81 -0
  29. package/apps/backend/dist/routes/connect.js +1 -0
  30. package/apps/backend/dist/routes/docs.js +13 -0
  31. package/apps/backend/dist/routes/health.js +6 -0
  32. package/apps/backend/dist/routes/library.js +139 -0
  33. package/apps/backend/dist/routes/projects.js +95 -0
  34. package/apps/backend/dist/routes/tasks.js +522 -0
  35. package/apps/backend/dist/routes/web.js +79 -0
  36. package/apps/backend/dist/routes/workflow-templates.js +152 -0
  37. package/apps/backend/dist/routes/workflow.js +165 -0
  38. package/apps/backend/dist/services/connect-target.js +4 -0
  39. package/apps/backend/dist/services/epic-service.js +269 -0
  40. package/apps/backend/dist/services/library-service.js +222 -0
  41. package/apps/backend/dist/services/project-registry.js +122 -0
  42. package/apps/backend/dist/services/task-assignee-service.js +42 -0
  43. package/apps/backend/dist/services/task-claim-policy.js +310 -0
  44. package/apps/backend/dist/services/task-queue.js +105 -0
  45. package/apps/backend/dist/services/task-service.js +198 -0
  46. package/apps/backend/dist/services/workflow-rules.js +18 -0
  47. package/apps/backend/dist/services/workflow-service.js +418 -0
  48. package/apps/backend/dist/services/workflow-spawn-service.js +179 -0
  49. package/apps/backend/dist/services/workflow-state-service.js +157 -0
  50. package/apps/backend/dist/services/workflow-template-service.js +204 -0
  51. package/apps/backend/public/assets/index-Bl1ciVpY.js +409 -0
  52. package/apps/backend/public/assets/index-ByKECv-I.css +1 -0
  53. package/apps/backend/public/index.html +13 -0
  54. package/bin/task-bridge.mjs +86 -0
  55. package/package.json +41 -0
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ import { AppError } from "../errors/app-error.js";
3
+ import { assertAuth } from "../middleware/auth.js";
4
+ import { listPublicUsers, createUser, updateUser, deleteUser, listUserRows, readUserToken, } from "../db/users-db.js";
5
+ const createUserSchema = z.object({
6
+ name: z.string().trim().min(1),
7
+ email: z.string().trim().toLowerCase().email(),
8
+ password: z.string().min(6),
9
+ role: z.enum(["admin", "read-write", "read"]).default("read-write"),
10
+ });
11
+ const updateUserSchema = z.object({
12
+ name: z.string().trim().min(1),
13
+ role: z.enum(["admin", "read-write", "read"]),
14
+ });
15
+ function requireAdmin(request) {
16
+ const user = assertAuth(request);
17
+ if (user.role !== "admin") {
18
+ throw new AppError("Admin access required", 403);
19
+ }
20
+ return user;
21
+ }
22
+ export function adminUserRoutes(app) {
23
+ app.get("/admin/users", (request) => {
24
+ requireAdmin(request);
25
+ return { users: listPublicUsers() };
26
+ });
27
+ app.post("/admin/users", async (request, reply) => {
28
+ requireAdmin(request);
29
+ const body = createUserSchema.parse(request.body);
30
+ const user = createUser({
31
+ name: body.name,
32
+ email: body.email,
33
+ password: body.password,
34
+ role: body.role,
35
+ isSystemAdmin: false,
36
+ mustChangePassword: true,
37
+ });
38
+ return reply.status(201).send({ user });
39
+ });
40
+ app.patch("/admin/users/:userId", (request) => {
41
+ requireAdmin(request);
42
+ const { userId } = request.params;
43
+ const existingRows = listUserRows({ id: userId, email: "", token: "" });
44
+ if (existingRows.length === 0)
45
+ throw new AppError("User not found", 404);
46
+ const existing = existingRows[0];
47
+ if (!existing)
48
+ throw new AppError("User not found", 404);
49
+ const body = updateUserSchema.parse(request.body);
50
+ if (existing.is_system_admin && body.role !== "admin") {
51
+ throw new AppError("Cannot change system admin role", 400);
52
+ }
53
+ const updated = updateUser(userId, body);
54
+ if (updated === null)
55
+ throw new AppError("User not found", 404);
56
+ return { user: updated };
57
+ });
58
+ app.delete("/admin/users/:userId", async (request, reply) => {
59
+ requireAdmin(request);
60
+ const { userId } = request.params;
61
+ const result = deleteUser(userId);
62
+ if (!result.deleted) {
63
+ let reason = "Cannot delete user";
64
+ if (result.reason !== "") {
65
+ reason = result.reason;
66
+ }
67
+ throw new AppError(reason, 400);
68
+ }
69
+ return reply.status(204).send();
70
+ });
71
+ app.get("/admin/users/:userId/token", (request) => {
72
+ requireAdmin(request);
73
+ const { userId } = request.params;
74
+ const token = readUserToken(userId);
75
+ if (token === "")
76
+ throw new AppError("User not found", 404);
77
+ return { token };
78
+ });
79
+ }
@@ -0,0 +1,81 @@
1
+ import { z } from "zod";
2
+ import { AppError } from "../errors/app-error.js";
3
+ import { hasAnyUser, createUser, listUserRows, verifyPassword, readUserToken, updateUserPassword, } from "../db/users-db.js";
4
+ import { resolveAuthUser } from "../middleware/auth.js";
5
+ const setupSchema = z.object({
6
+ name: z.string().trim().min(1),
7
+ email: z.string().trim().toLowerCase().email(),
8
+ password: z.string().min(6),
9
+ });
10
+ const loginSchema = z.object({
11
+ email: z.string().trim().toLowerCase().email(),
12
+ password: z.string().min(1),
13
+ });
14
+ const changePasswordSchema = z.object({
15
+ currentPassword: z.string().min(1),
16
+ newPassword: z.string().min(6),
17
+ });
18
+ function mapAuthUser(userRow) {
19
+ return {
20
+ id: userRow.id,
21
+ name: userRow.name,
22
+ email: userRow.email,
23
+ role: userRow.role,
24
+ isSystemAdmin: userRow.is_system_admin === 1,
25
+ mustChangePassword: userRow.must_change_password === 1,
26
+ };
27
+ }
28
+ export function authRoutes(app) {
29
+ app.get("/auth/status", () => {
30
+ return { hasUsers: hasAnyUser() };
31
+ });
32
+ app.post("/auth/setup", async (request, reply) => {
33
+ if (hasAnyUser()) {
34
+ throw new AppError("Setup already completed", 409);
35
+ }
36
+ const body = setupSchema.parse(request.body);
37
+ const user = createUser({
38
+ name: body.name,
39
+ email: body.email,
40
+ password: body.password,
41
+ role: "admin",
42
+ isSystemAdmin: true,
43
+ mustChangePassword: false,
44
+ });
45
+ return reply.status(201).send({ user, message: "Admin account created" });
46
+ });
47
+ app.post("/auth/login", async (request, reply) => {
48
+ const body = loginSchema.parse(request.body);
49
+ const userRows = listUserRows({ id: "", email: body.email, token: "" });
50
+ const firstRow = userRows[0];
51
+ if (userRows.length === 0 || !firstRow || !verifyPassword(firstRow, body.password)) {
52
+ throw new AppError("Invalid email or password", 401);
53
+ }
54
+ const userRow = firstRow;
55
+ const token = readUserToken(userRow.id);
56
+ if (token === "")
57
+ throw new AppError("Token not found", 500);
58
+ return reply.send({
59
+ token,
60
+ user: mapAuthUser(userRow),
61
+ });
62
+ });
63
+ app.get("/auth/me", (request) => {
64
+ const userRow = resolveAuthUser(request);
65
+ return mapAuthUser(userRow);
66
+ });
67
+ app.post("/auth/change-password", (request) => {
68
+ const userRow = resolveAuthUser(request);
69
+ const body = changePasswordSchema.parse(request.body);
70
+ if (!verifyPassword(userRow, body.currentPassword)) {
71
+ throw new AppError("Invalid current password", 401);
72
+ }
73
+ if (body.currentPassword === body.newPassword) {
74
+ throw new AppError("New password must be different", 400);
75
+ }
76
+ updateUserPassword(userRow.id, body.newPassword);
77
+ return {
78
+ user: Object.assign({}, mapAuthUser(userRow), { mustChangePassword: false }),
79
+ };
80
+ });
81
+ }
@@ -0,0 +1 @@
1
+ export async function connectRoutes(_app) { }
@@ -0,0 +1,13 @@
1
+ import { openapiSpec } from "../openapi.js";
2
+ /**
3
+ * GET /api/docs — returns the OpenAPI 3.1 spec as raw JSON.
4
+ * No auth required. No Swagger UI — just the machine-readable spec.
5
+ */
6
+ export function docsRoutes(app) {
7
+ app.get("/docs", async (_request, reply) => {
8
+ return reply
9
+ .header("Content-Type", "application/json; charset=utf-8")
10
+ .header("Cache-Control", "no-cache")
11
+ .send(JSON.stringify(openapiSpec, null, 2));
12
+ });
13
+ }
@@ -0,0 +1,6 @@
1
+ export function healthRoutes(app) {
2
+ app.get("/health", () => ({
3
+ status: "ok",
4
+ timestamp: new Date().toISOString(),
5
+ }));
6
+ }
@@ -0,0 +1,139 @@
1
+ import { z } from "zod";
2
+ import { assertAuth } from "../middleware/auth.js";
3
+ import { createLibrary, createLibraryDocument, getLibrary, getLibraryDocument, linkDocumentToTask, listLibraries, listTaskLibraryLinks, removeLibrary, removeLibraryDocument, unlinkDocumentFromTask, updateLibrary, updateLibraryDocument, } from "../services/library-service.js";
4
+ const libraryIdParamsSchema = z.object({
5
+ libraryId: z.string().trim().min(1),
6
+ });
7
+ const documentIdParamsSchema = z.object({
8
+ documentId: z.string().trim().min(1),
9
+ });
10
+ const libraryDocumentParamsSchema = z.object({
11
+ libraryId: z.string().trim().min(1),
12
+ documentId: z.string().trim().min(1),
13
+ });
14
+ const taskIdParamsSchema = z.object({
15
+ taskId: z.coerce.number().int().positive(),
16
+ });
17
+ const createLibrarySchema = z.object({
18
+ id: z.string().trim().default(""),
19
+ title: z.string().trim().min(1),
20
+ description: z.string().trim().default(""),
21
+ });
22
+ const updateLibrarySchema = z.object({
23
+ title: z.string().trim().min(1),
24
+ description: z.string().trim().default(""),
25
+ });
26
+ const createDocumentSchema = z.object({
27
+ id: z.string().trim().default(""),
28
+ title: z.string().trim().min(1),
29
+ description: z.string().trim().default(""),
30
+ });
31
+ const linkDocumentSchema = z.object({
32
+ taskId: z.coerce.number().int().positive(),
33
+ });
34
+ export function libraryRoutes(app) {
35
+ app.get("/libraries", (request) => {
36
+ assertAuth(request);
37
+ return { items: listLibraries() };
38
+ });
39
+ app.post("/libraries", async (request, reply) => {
40
+ assertAuth(request);
41
+ let rawBody = request.body;
42
+ if (rawBody === null) {
43
+ rawBody = {};
44
+ }
45
+ const body = createLibrarySchema.parse(rawBody);
46
+ const library = createLibrary(body);
47
+ return reply.status(201).send(library);
48
+ });
49
+ app.get("/libraries/:libraryId", async (request, reply) => {
50
+ assertAuth(request);
51
+ const { libraryId } = libraryIdParamsSchema.parse(request.params);
52
+ const library = getLibrary(libraryId);
53
+ if (!library)
54
+ return reply.status(404).send({ error: "Library not found" });
55
+ return library;
56
+ });
57
+ app.put("/libraries/:libraryId", (request) => {
58
+ assertAuth(request);
59
+ const { libraryId } = libraryIdParamsSchema.parse(request.params);
60
+ let rawBody = request.body;
61
+ if (rawBody === null) {
62
+ rawBody = {};
63
+ }
64
+ const body = updateLibrarySchema.parse(rawBody);
65
+ return updateLibrary(libraryId, body);
66
+ });
67
+ app.delete("/libraries/:libraryId", async (request, reply) => {
68
+ assertAuth(request);
69
+ const { libraryId } = libraryIdParamsSchema.parse(request.params);
70
+ removeLibrary(libraryId);
71
+ return reply.status(204).send();
72
+ });
73
+ app.post("/libraries/:libraryId/documents", async (request, reply) => {
74
+ assertAuth(request);
75
+ const { libraryId } = libraryIdParamsSchema.parse(request.params);
76
+ let rawBody = request.body;
77
+ if (rawBody === null) {
78
+ rawBody = {};
79
+ }
80
+ const body = createDocumentSchema.parse(rawBody);
81
+ const document = createLibraryDocument(libraryId, body);
82
+ return reply.status(201).send(document);
83
+ });
84
+ app.get("/library-documents/:documentId", async (request, reply) => {
85
+ assertAuth(request);
86
+ const { documentId } = documentIdParamsSchema.parse(request.params);
87
+ const document = getLibraryDocument(documentId);
88
+ if (!document)
89
+ return reply.status(404).send({ error: "Document not found" });
90
+ return document;
91
+ });
92
+ app.put("/libraries/:libraryId/documents/:documentId", async (request, reply) => {
93
+ assertAuth(request);
94
+ const { libraryId, documentId } = libraryDocumentParamsSchema.parse(request.params);
95
+ let rawBody = request.body;
96
+ if (rawBody === null) {
97
+ rawBody = {};
98
+ }
99
+ const body = updateLibrarySchema.parse(rawBody);
100
+ const existing = getLibraryDocument(documentId);
101
+ if (!existing || existing.libraryId !== libraryId) {
102
+ return reply.status(404).send({ error: "Document not found" });
103
+ }
104
+ return updateLibraryDocument(documentId, body);
105
+ });
106
+ app.delete("/libraries/:libraryId/documents/:documentId", async (request, reply) => {
107
+ assertAuth(request);
108
+ const { libraryId, documentId } = libraryDocumentParamsSchema.parse(request.params);
109
+ const existing = getLibraryDocument(documentId);
110
+ if (!existing || existing.libraryId !== libraryId) {
111
+ return reply.status(404).send({ error: "Document not found" });
112
+ }
113
+ removeLibraryDocument(documentId);
114
+ return reply.status(204).send();
115
+ });
116
+ app.post("/library-documents/:documentId/links", async (request, reply) => {
117
+ assertAuth(request);
118
+ const { documentId } = documentIdParamsSchema.parse(request.params);
119
+ let rawBody = request.body;
120
+ if (rawBody === null) {
121
+ rawBody = {};
122
+ }
123
+ const body = linkDocumentSchema.parse(rawBody);
124
+ const links = linkDocumentToTask(documentId, body.taskId);
125
+ return reply.status(201).send({ items: links });
126
+ });
127
+ app.delete("/library-documents/:documentId/links/:taskId", async (request, reply) => {
128
+ assertAuth(request);
129
+ const { documentId } = documentIdParamsSchema.parse(request.params);
130
+ const { taskId } = taskIdParamsSchema.parse(request.params);
131
+ unlinkDocumentFromTask(documentId, taskId);
132
+ return reply.status(204).send();
133
+ });
134
+ app.get("/tasks/:taskId/library-links", (request) => {
135
+ assertAuth(request);
136
+ const { taskId } = taskIdParamsSchema.parse(request.params);
137
+ return { items: listTaskLibraryLinks(taskId) };
138
+ });
139
+ }
@@ -0,0 +1,95 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_WORKFLOW_TEMPLATE_ID } from "../domain/workflow-template-id.js";
3
+ import { isAppError } from "../errors/app-error.js";
4
+ import { assertAuth } from "../middleware/auth.js";
5
+ import { createProject, listPublicProjects, refreshProjectRegistry, updateProject, updateProjectRepoPath, } from "../services/project-registry.js";
6
+ const projectIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
7
+ const createProjectSchema = z.object({
8
+ name: z.string().trim().min(1),
9
+ id: z
10
+ .string()
11
+ .trim()
12
+ .default("")
13
+ .refine((value) => value === "" || projectIdPattern.test(value)),
14
+ repoPath: z.string().trim().min(1),
15
+ description: z.string().trim().default(""),
16
+ workflowTemplateId: z.string().trim().default(DEFAULT_WORKFLOW_TEMPLATE_ID),
17
+ });
18
+ const updateProjectSchema = z.object({
19
+ name: z.string().trim().min(1),
20
+ repoPath: z.string().trim().min(1),
21
+ description: z.string().trim(),
22
+ workflowTemplateId: z.string().trim(),
23
+ });
24
+ const updateRepoPathSchema = z.object({
25
+ repoPath: z.string().trim().min(1),
26
+ });
27
+ const projectIdParamsSchema = z.object({
28
+ id: z.string().trim().min(1),
29
+ });
30
+ export function projectRoutes(app) {
31
+ app.get("/projects", (request) => {
32
+ assertAuth(request);
33
+ refreshProjectRegistry();
34
+ return { projects: listPublicProjects() };
35
+ });
36
+ app.post("/projects", async (request, reply) => {
37
+ assertAuth(request);
38
+ let postBody = request.body;
39
+ if (postBody === null) {
40
+ postBody = {};
41
+ }
42
+ const body = createProjectSchema.parse(postBody);
43
+ const created = createProject(body);
44
+ if (created === "duplicate") {
45
+ return reply.status(409).send({ error: "Project id already exists" });
46
+ }
47
+ if (!created) {
48
+ return reply.status(400).send({ error: "Invalid project" });
49
+ }
50
+ return reply.status(201).send(created);
51
+ });
52
+ app.patch("/projects/:id", async (request, reply) => {
53
+ assertAuth(request);
54
+ const { id } = projectIdParamsSchema.parse(request.params);
55
+ let patchBody = request.body;
56
+ if (patchBody === null) {
57
+ patchBody = {};
58
+ }
59
+ const body = updateProjectSchema.parse(patchBody);
60
+ try {
61
+ const updated = updateProject(id, body);
62
+ if (!updated) {
63
+ return reply.status(404).send({ error: "Project not found" });
64
+ }
65
+ return {
66
+ id: updated.id,
67
+ name: updated.name,
68
+ repoPath: updated.repoPath,
69
+ description: updated.description,
70
+ workflowTemplateId: updated.workflowTemplateId,
71
+ };
72
+ }
73
+ catch (error) {
74
+ const handled = error;
75
+ if (isAppError(handled)) {
76
+ return reply.status(handled.statusCode).send({ error: handled.message });
77
+ }
78
+ throw error;
79
+ }
80
+ });
81
+ app.put("/projects/:id/repo-path", async (request, reply) => {
82
+ assertAuth(request);
83
+ const { id } = projectIdParamsSchema.parse(request.params);
84
+ const body = updateRepoPathSchema.parse(request.body);
85
+ const updated = updateProjectRepoPath(id, body.repoPath);
86
+ if (!updated) {
87
+ return reply.status(404).send({ error: "Project not found" });
88
+ }
89
+ return {
90
+ id: updated.id,
91
+ name: updated.name,
92
+ repoPath: updated.repoPath,
93
+ };
94
+ });
95
+ }