teamcopilot 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 (209) hide show
  1. package/.env.example +10 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +131 -0
  4. package/bin/teamcopilot.js +281 -0
  5. package/dist/auth/index.js +189 -0
  6. package/dist/change-user-role.js +77 -0
  7. package/dist/chat/index.js +849 -0
  8. package/dist/constants.js +2 -0
  9. package/dist/create-user.js +98 -0
  10. package/dist/cronjob/index.js +16 -0
  11. package/dist/cronjob/resource-reconciliation.js +33 -0
  12. package/dist/delete-user.js +66 -0
  13. package/dist/frontend/assets/abap-CRCWOmpq.js +1 -0
  14. package/dist/frontend/assets/apex-DnsZk_dE.js +1 -0
  15. package/dist/frontend/assets/azcli-1IWB1ccx.js +1 -0
  16. package/dist/frontend/assets/bat-DPkNLes8.js +1 -0
  17. package/dist/frontend/assets/bicep-Corcdgou.js +2 -0
  18. package/dist/frontend/assets/cameligo-CGrWLZr3.js +1 -0
  19. package/dist/frontend/assets/clojure-D9WOWImG.js +1 -0
  20. package/dist/frontend/assets/codicon-DCmgc-ay.ttf +0 -0
  21. package/dist/frontend/assets/coffee-B7EJu28W.js +1 -0
  22. package/dist/frontend/assets/cpp-SEyurbux.js +1 -0
  23. package/dist/frontend/assets/csharp-BoL64M5l.js +1 -0
  24. package/dist/frontend/assets/csp-C46ZqvIl.js +1 -0
  25. package/dist/frontend/assets/css-DQU6DXDx.js +3 -0
  26. package/dist/frontend/assets/cssMode-BDT3WbVs.js +4 -0
  27. package/dist/frontend/assets/cypher-D84EuPTj.js +1 -0
  28. package/dist/frontend/assets/dart-D8lhlL1r.js +1 -0
  29. package/dist/frontend/assets/dockerfile-DLk6rpji.js +1 -0
  30. package/dist/frontend/assets/ecl-BO6FnfXk.js +1 -0
  31. package/dist/frontend/assets/editor.worker-B4pQIWZD.js +12 -0
  32. package/dist/frontend/assets/elixir-BRjLKONM.js +1 -0
  33. package/dist/frontend/assets/flow9-Cac8vKd7.js +1 -0
  34. package/dist/frontend/assets/freemarker2-C7-hEgID.js +3 -0
  35. package/dist/frontend/assets/fsharp-fd1GTHhf.js +1 -0
  36. package/dist/frontend/assets/go-O9LJTZXk.js +1 -0
  37. package/dist/frontend/assets/graphql-LQdxqEYJ.js +1 -0
  38. package/dist/frontend/assets/handlebars-4cwTkPir.js +1 -0
  39. package/dist/frontend/assets/hcl-DxDQ3s82.js +1 -0
  40. package/dist/frontend/assets/html-YNfE1Q0A.js +1 -0
  41. package/dist/frontend/assets/htmlMode-opTQ1HoB.js +4 -0
  42. package/dist/frontend/assets/index-DWyaVa1h.js +782 -0
  43. package/dist/frontend/assets/index-lXrsgeTF.css +1 -0
  44. package/dist/frontend/assets/ini-BvajGCUy.js +1 -0
  45. package/dist/frontend/assets/java-SYsfObOQ.js +1 -0
  46. package/dist/frontend/assets/javascript-BEwGzk7T.js +1 -0
  47. package/dist/frontend/assets/jsonMode-CGhIS5Al.js +10 -0
  48. package/dist/frontend/assets/julia-DQXNmw_w.js +1 -0
  49. package/dist/frontend/assets/kotlin-qQ0MG-9I.js +1 -0
  50. package/dist/frontend/assets/less-GGFNNJHn.js +2 -0
  51. package/dist/frontend/assets/lexon-Canl7DCW.js +1 -0
  52. package/dist/frontend/assets/liquid-QekTGCGJ.js +1 -0
  53. package/dist/frontend/assets/lua-D28Ae8-K.js +1 -0
  54. package/dist/frontend/assets/m3-DPitgjJI.js +1 -0
  55. package/dist/frontend/assets/markdown-B811l8j2.js +1 -0
  56. package/dist/frontend/assets/mdx-BAVDaB7v.js +1 -0
  57. package/dist/frontend/assets/mips-CdjsipkG.js +1 -0
  58. package/dist/frontend/assets/msdax-CYqgjx_P.js +1 -0
  59. package/dist/frontend/assets/mysql-BHd6q0vd.js +1 -0
  60. package/dist/frontend/assets/objective-c-B1aVtJYH.js +1 -0
  61. package/dist/frontend/assets/pascal-BhNW15KB.js +1 -0
  62. package/dist/frontend/assets/pascaligo-5jv8CcQD.js +1 -0
  63. package/dist/frontend/assets/perl-DlYyT36c.js +1 -0
  64. package/dist/frontend/assets/pgsql-Dy0bjov7.js +1 -0
  65. package/dist/frontend/assets/php-120yhfDK.js +1 -0
  66. package/dist/frontend/assets/pla-CjnFlu4u.js +1 -0
  67. package/dist/frontend/assets/postiats-CQpG440k.js +1 -0
  68. package/dist/frontend/assets/powerquery-DdJtto1Z.js +1 -0
  69. package/dist/frontend/assets/powershell-Bu_VLpJB.js +1 -0
  70. package/dist/frontend/assets/protobuf-IBS6jZEB.js +2 -0
  71. package/dist/frontend/assets/pug-kFxLfcjb.js +1 -0
  72. package/dist/frontend/assets/python-BQlHw7XO.js +1 -0
  73. package/dist/frontend/assets/qsharp-q7JyzKFN.js +1 -0
  74. package/dist/frontend/assets/r-BIFz-_sK.js +1 -0
  75. package/dist/frontend/assets/razor-Be3Wwc2E.js +1 -0
  76. package/dist/frontend/assets/redis-CHOsPHWR.js +1 -0
  77. package/dist/frontend/assets/redshift-CBifECDb.js +1 -0
  78. package/dist/frontend/assets/restructuredtext-CghPJEOS.js +1 -0
  79. package/dist/frontend/assets/ruby-CYWGW-b1.js +1 -0
  80. package/dist/frontend/assets/rust-DMDD0SHb.js +1 -0
  81. package/dist/frontend/assets/sb-BYAiYHFx.js +1 -0
  82. package/dist/frontend/assets/scala-Bqvq8jcR.js +1 -0
  83. package/dist/frontend/assets/scheme-Dhb-2j9p.js +1 -0
  84. package/dist/frontend/assets/scss-CTwUZ5N7.js +3 -0
  85. package/dist/frontend/assets/shell-CsDZo4DB.js +1 -0
  86. package/dist/frontend/assets/solidity-CME5AdoB.js +1 -0
  87. package/dist/frontend/assets/sophia-RYC1BQQz.js +1 -0
  88. package/dist/frontend/assets/sparql-KEyrF7De.js +1 -0
  89. package/dist/frontend/assets/sql-BdTr02Mf.js +1 -0
  90. package/dist/frontend/assets/st-C7iG7M4S.js +1 -0
  91. package/dist/frontend/assets/swift-D7IUmUK8.js +1 -0
  92. package/dist/frontend/assets/systemverilog-DgMryOEJ.js +1 -0
  93. package/dist/frontend/assets/tcl-PloMZuKG.js +1 -0
  94. package/dist/frontend/assets/tsMode-CIBFoN3z.js +11 -0
  95. package/dist/frontend/assets/twig-BfRIq3la.js +1 -0
  96. package/dist/frontend/assets/typescript-BuV9wEIE.js +1 -0
  97. package/dist/frontend/assets/typespec-CzxlYoT_.js +1 -0
  98. package/dist/frontend/assets/vb-BwAE3J76.js +1 -0
  99. package/dist/frontend/assets/wgsl-B_1kOXbF.js +298 -0
  100. package/dist/frontend/assets/xml-DcDKYaM4.js +1 -0
  101. package/dist/frontend/assets/yaml-CuBNmOuI.js +1 -0
  102. package/dist/frontend/index.html +14 -0
  103. package/dist/frontend/logo.svg +50 -0
  104. package/dist/index.js +169 -0
  105. package/dist/logging.js +30 -0
  106. package/dist/opencode-auth/index.js +122 -0
  107. package/dist/opencode-server.js +91 -0
  108. package/dist/prisma/client.js +38 -0
  109. package/dist/reset-password.js +73 -0
  110. package/dist/rotate-jwt-secret.js +20 -0
  111. package/dist/scripts/prisma-workspace.js +34 -0
  112. package/dist/skills/index.js +311 -0
  113. package/dist/types/permissions.js +2 -0
  114. package/dist/types/shared/permissions.js +17 -0
  115. package/dist/types/shared/skill.js +17 -0
  116. package/dist/types/shared/workflow-files.js +17 -0
  117. package/dist/types/shared/workflow.js +17 -0
  118. package/dist/types/skill.js +2 -0
  119. package/dist/types/workflow-files.js +2 -0
  120. package/dist/types/workflow.js +2 -0
  121. package/dist/users/index.js +22 -0
  122. package/dist/utils/approval-snapshot-common.js +596 -0
  123. package/dist/utils/assert.js +20 -0
  124. package/dist/utils/chat-session.js +44 -0
  125. package/dist/utils/cli-bootstrap.js +26 -0
  126. package/dist/utils/index.js +95 -0
  127. package/dist/utils/jwt-secret.js +63 -0
  128. package/dist/utils/opencode-auth.js +126 -0
  129. package/dist/utils/opencode-client.js +109 -0
  130. package/dist/utils/password-policy.js +12 -0
  131. package/dist/utils/permission-common.js +280 -0
  132. package/dist/utils/redact.js +108 -0
  133. package/dist/utils/resource-access.js +37 -0
  134. package/dist/utils/resource-file-routes.js +115 -0
  135. package/dist/utils/resource-files.js +572 -0
  136. package/dist/utils/runtime-paths.js +61 -0
  137. package/dist/utils/session-abort.js +52 -0
  138. package/dist/utils/skill-approval-snapshot.js +39 -0
  139. package/dist/utils/skill-files.js +17 -0
  140. package/dist/utils/skill-permissions.js +15 -0
  141. package/dist/utils/skill.js +217 -0
  142. package/dist/utils/user-role.js +14 -0
  143. package/dist/utils/workflow-approval-snapshot.js +38 -0
  144. package/dist/utils/workflow-files.js +17 -0
  145. package/dist/utils/workflow-interruption.js +50 -0
  146. package/dist/utils/workflow-permissions.js +27 -0
  147. package/dist/utils/workflow-runner.js +414 -0
  148. package/dist/utils/workflow.js +158 -0
  149. package/dist/utils/workspace-sync.js +204 -0
  150. package/dist/workflows/index.js +751 -0
  151. package/dist/workspace_files/.opencode/opencode.json +17 -0
  152. package/dist/workspace_files/.opencode/package.json +14 -0
  153. package/dist/workspace_files/.opencode/plugins/createSkill.ts +339 -0
  154. package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +345 -0
  155. package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +173 -0
  156. package/dist/workspace_files/.opencode/plugins/findSkill.ts +211 -0
  157. package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +135 -0
  158. package/dist/workspace_files/.opencode/plugins/honeytoken-protection.ts +64 -0
  159. package/dist/workspace_files/.opencode/plugins/listAvailableSkills.ts +93 -0
  160. package/dist/workspace_files/.opencode/plugins/listAvailableWorkflows.ts +93 -0
  161. package/dist/workspace_files/.opencode/plugins/python-protection.ts +184 -0
  162. package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +168 -0
  163. package/dist/workspace_files/.opencode/tsconfig.json +16 -0
  164. package/dist/workspace_files/AGENTS.md +483 -0
  165. package/dist/workspace_files/package-lock.json +167 -0
  166. package/dist/workspace_files/package.json +5 -0
  167. package/package.json +86 -0
  168. package/prisma/migrations/20260203040755_init/migration.sql +20 -0
  169. package/prisma/migrations/20260204034845_replace_google_auth_with_email_password/migration.sql +25 -0
  170. package/prisma/migrations/20260207022226_add_user_role/migration.sql +25 -0
  171. package/prisma/migrations/20260210161254_add_workflow_runs/migration.sql +16 -0
  172. package/prisma/migrations/20260211050606_adds_workflow_table/migration.sql +40 -0
  173. package/prisma/migrations/20260211050750_adds_fkey_constraint/migration.sql +21 -0
  174. package/prisma/migrations/20260211051912_removes_workflow_table/migration.sql +34 -0
  175. package/prisma/migrations/20260211052238_changes_workflow_id_to_slug/migration.sql +27 -0
  176. package/prisma/migrations/20260212051912_add_output_to_workflow_runs/migration.sql +2 -0
  177. package/prisma/migrations/20260213073006_add_chat_sessions/migration.sql +13 -0
  178. package/prisma/migrations/20260216053202_add_chat_sessions_opencode_session_id_idx/migration.sql +2 -0
  179. package/prisma/migrations/20260216053237_drop_redundant_chat_sessions_opencode_idx/migration.sql +2 -0
  180. package/prisma/migrations/20260219060705_makes/migration.sql +24 -0
  181. package/prisma/migrations/20260222040542_add_workflow_execution_permissions/migration.sql +18 -0
  182. package/prisma/migrations/20260222040815_remove_workflow_execution_permissions/migration.sql +10 -0
  183. package/prisma/migrations/20260222041348_add_workflow_execution_permissions_final/migration.sql +17 -0
  184. package/prisma/migrations/20260222041741_rename_to_tool_execution_permissions/migration.sql +30 -0
  185. package/prisma/migrations/20260222041826_simplify_tool_execution_permissions/migration.sql +29 -0
  186. package/prisma/migrations/20260222041950_add_fields_for_standalone_permissions/migration.sql +32 -0
  187. package/prisma/migrations/20260222042954_simplify_tool_permissions_table/migration.sql +27 -0
  188. package/prisma/migrations/20260223073902_add_workflow_run_permissions_tables/migration.sql +23 -0
  189. package/prisma/migrations/20260225025151_add_workflow_metadata/migration.sql +16 -0
  190. package/prisma/migrations/20260225031035_merge_workflow_permissions_into_metadata/migration.sql +44 -0
  191. package/prisma/migrations/20260225031752_removes_default_for_run_permission_mode/migration.sql +20 -0
  192. package/prisma/migrations/20260225033603_remove_workflow_metadata_user_fkeys/migration.sql +18 -0
  193. package/prisma/migrations/20260225043032_restore_workflow_metadata_user_fkeys/migration.sql +20 -0
  194. package/prisma/migrations/20260225091423_add_workflow_approved_snapshots/migration.sql +28 -0
  195. package/prisma/migrations/20260226032121_add_is_approved_to_workflow_metadata/migration.sql +21 -0
  196. package/prisma/migrations/20260226032444_undoes_last_db_change/migration.sql +26 -0
  197. package/prisma/migrations/20260227120000_remove_snapshot_hash_from_approved_snapshots/migration.sql +16 -0
  198. package/prisma/migrations/20260228071125_adds_workspace_path_to_snapshot_table/migration.sql +22 -0
  199. package/prisma/migrations/20260228071217_modifies_index_and_removes_default_value/migration.sql +22 -0
  200. package/prisma/migrations/20260228071710_undoes_previous/migration.sql +27 -0
  201. package/prisma/migrations/20260228105022_add_must_change_password_first_login/migration.sql +20 -0
  202. package/prisma/migrations/20260301115439_add_workflow_run_log_refs/migration.sql +8 -0
  203. package/prisma/migrations/20260301122557_add_workflow_aborted_sessions/migration.sql +5 -0
  204. package/prisma/migrations/20260302045545_move_workflow_run_log_refs_into_workflow_runs/migration.sql +17 -0
  205. package/prisma/migrations/20260303040318_add_skill_tables/migration.sql +61 -0
  206. package/prisma/migrations/20260303051533_unify_resource_permissions/migration.sql +97 -0
  207. package/prisma/migrations/20260303064255_unify_resource_metadata_and_snapshots/migration.sql +179 -0
  208. package/prisma/migrations/migration_lock.toml +3 -0
  209. package/prisma/schema.prisma +147 -0
@@ -0,0 +1,751 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = __importDefault(require("express"));
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const promises_1 = __importDefault(require("fs/promises"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const crypto_1 = require("crypto");
12
+ const multer_1 = __importDefault(require("multer"));
13
+ const client_1 = __importDefault(require("../prisma/client"));
14
+ const index_1 = require("../utils/index");
15
+ const workflow_1 = require("../utils/workflow");
16
+ const workflow_files_1 = require("../utils/workflow-files");
17
+ const workflow_permissions_1 = require("../utils/workflow-permissions");
18
+ const permission_common_1 = require("../utils/permission-common");
19
+ const workflow_approval_snapshot_1 = require("../utils/workflow-approval-snapshot");
20
+ const workspace_sync_1 = require("../utils/workspace-sync");
21
+ const workflow_runner_1 = require("../utils/workflow-runner");
22
+ const workflow_interruption_1 = require("../utils/workflow-interruption");
23
+ const session_abort_1 = require("../utils/session-abort");
24
+ const resource_file_routes_1 = require("../utils/resource-file-routes");
25
+ const resource_access_1 = require("../utils/resource-access");
26
+ const router = express_1.default.Router({ mergeParams: true });
27
+ const uploadTmpDir = path_1.default.join(os_1.default.tmpdir(), "teamcopilot-workflow-uploads");
28
+ fs_1.default.mkdirSync(uploadTmpDir, { recursive: true });
29
+ const maxUploadBytes = (() => {
30
+ const parsed = Number(process.env.WORKFLOW_FILE_UPLOAD_MAX_MB ?? "1024");
31
+ if (!Number.isFinite(parsed) || parsed <= 0) {
32
+ return 1024 * 1024 * 1024;
33
+ }
34
+ return Math.floor(parsed * 1024 * 1024);
35
+ })();
36
+ const workflowFileUpload = (0, multer_1.default)({
37
+ dest: uploadTmpDir,
38
+ limits: {
39
+ files: 1,
40
+ fileSize: maxUploadBytes,
41
+ },
42
+ });
43
+ function isPathInside(childPath, parentPath) {
44
+ const parent = path_1.default.resolve(parentPath) + path_1.default.sep;
45
+ const child = path_1.default.resolve(childPath) + path_1.default.sep;
46
+ return child.startsWith(parent);
47
+ }
48
+ function sanitizeFilenamePart(value) {
49
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
50
+ }
51
+ async function assertCurrentUserCanRunWorkflow(slug, userId) {
52
+ await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
53
+ const approvalState = await (0, workflow_approval_snapshot_1.getWorkflowSnapshotApprovalState)(slug);
54
+ if (!approvalState.is_current_code_approved) {
55
+ throw {
56
+ status: 403,
57
+ message: 'Workflow is not approved for the current code version'
58
+ };
59
+ }
60
+ const permissionSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, userId);
61
+ if (!permissionSummary.can_edit) {
62
+ throw {
63
+ status: 403,
64
+ message: permissionSummary.is_locked_due_to_missing_users
65
+ ? 'Workflow cannot be run because no allowed users remain'
66
+ : 'You do not have permission to run this workflow. Please contact the workflow owner to request permission.'
67
+ };
68
+ }
69
+ }
70
+ async function getWorkflowEditorAccess(slug, userId) {
71
+ const accessSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, userId);
72
+ const workflowStatus = accessSummary.is_approved ? "approved" : "pending";
73
+ return {
74
+ can_view: accessSummary.can_view,
75
+ can_edit: accessSummary.can_edit,
76
+ editor_status: workflowStatus,
77
+ };
78
+ }
79
+ async function assertCanEditWorkflowFiles(slug, userId) {
80
+ const access = await getWorkflowEditorAccess(slug, userId);
81
+ if (!access.can_edit) {
82
+ throw {
83
+ status: 403,
84
+ message: "You do not have permission to edit this workflow"
85
+ };
86
+ }
87
+ }
88
+ async function assertCanViewWorkflowFiles(slug, userId) {
89
+ const access = await getWorkflowEditorAccess(slug, userId);
90
+ if (!access.can_view) {
91
+ throw {
92
+ status: 403,
93
+ message: "You do not have permission to view this workflow"
94
+ };
95
+ }
96
+ }
97
+ const workflowExecutions = new Map();
98
+ // GET /api/workflows - List available workflows from filesystem
99
+ router.get('/', (0, index_1.apiHandler)(async (req, res) => {
100
+ const slugs = (0, workflow_1.listWorkflowSlugs)();
101
+ const workflows = [];
102
+ const creatorIds = new Set();
103
+ const manifests = new Map();
104
+ const metadataBySlug = new Map();
105
+ for (const slug of slugs) {
106
+ const { manifest, metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
107
+ manifests.set(slug, manifest);
108
+ metadataBySlug.set(slug, metadata);
109
+ if (metadata.created_by_user_id) {
110
+ creatorIds.add(metadata.created_by_user_id);
111
+ }
112
+ }
113
+ const creators = creatorIds.size > 0
114
+ ? await client_1.default.users.findMany({
115
+ where: { id: { in: Array.from(creatorIds) } },
116
+ select: { id: true, name: true, email: true }
117
+ })
118
+ : [];
119
+ const creatorNameById = new Map(creators.map((creator) => [creator.id, creator.name]));
120
+ const creatorEmailById = new Map(creators.map((creator) => [creator.id, creator.email]));
121
+ for (const slug of slugs) {
122
+ const manifest = manifests.get(slug);
123
+ const metadata = metadataBySlug.get(slug);
124
+ if (!manifest)
125
+ continue;
126
+ if (!metadata)
127
+ continue;
128
+ const accessSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, req.userId);
129
+ if (!accessSummary.can_view) {
130
+ continue;
131
+ }
132
+ const createdByUserId = metadata.created_by_user_id;
133
+ workflows.push({
134
+ slug,
135
+ name: slug,
136
+ intent_summary: manifest.intent_summary,
137
+ created_by_user_id: createdByUserId,
138
+ created_by_user_name: createdByUserId ? (creatorNameById.get(createdByUserId) ?? null) : null,
139
+ created_by_user_email: createdByUserId ? (creatorEmailById.get(createdByUserId) ?? null) : null,
140
+ approved_by_user_id: metadata.approved_by_user_id ?? null,
141
+ is_approved: accessSummary.is_approved,
142
+ can_view: accessSummary.can_view,
143
+ can_edit: accessSummary.can_edit,
144
+ permission_mode: accessSummary.permission_mode,
145
+ is_locked_due_to_missing_users: accessSummary.is_locked_due_to_missing_users
146
+ });
147
+ }
148
+ res.json({ workflows });
149
+ }, true));
150
+ // GET /api/workflows/runs - List workflow run history (last 50, all users)
151
+ router.get('/runs', (0, index_1.apiHandler)(async (_req, res) => {
152
+ const runs = await client_1.default.workflow_runs.findMany({
153
+ orderBy: { started_at: 'desc' },
154
+ take: 50,
155
+ include: {
156
+ user: {
157
+ select: { name: true, email: true }
158
+ }
159
+ }
160
+ });
161
+ res.json({ runs });
162
+ }, true));
163
+ // GET /api/workflows/runs/logs - Get log file by run_id OR session_id+message_id
164
+ router.get('/runs/logs', (0, index_1.apiHandler)(async (req, res) => {
165
+ const runIdRaw = req.query.run_id;
166
+ const sessionIdRaw = req.query.session_id;
167
+ const messageIdRaw = req.query.message_id;
168
+ const runId = typeof runIdRaw === 'string' && runIdRaw.trim().length > 0 ? runIdRaw : null;
169
+ const sessionId = typeof sessionIdRaw === 'string' && sessionIdRaw.trim().length > 0 ? sessionIdRaw : null;
170
+ const messageId = typeof messageIdRaw === 'string' && messageIdRaw.trim().length > 0 ? messageIdRaw : null;
171
+ if (!runId && !(sessionId && messageId)) {
172
+ throw {
173
+ status: 400,
174
+ message: 'Provide either run_id, or both session_id and message_id'
175
+ };
176
+ }
177
+ if (runId && (sessionId || messageId)) {
178
+ throw {
179
+ status: 400,
180
+ message: 'Provide either run_id, or session_id + message_id, not both'
181
+ };
182
+ }
183
+ const run = runId
184
+ ? await client_1.default.workflow_runs.findUnique({
185
+ where: { id: runId },
186
+ select: { session_id: true, message_id: true }
187
+ })
188
+ : await client_1.default.workflow_runs.findFirst({
189
+ where: {
190
+ session_id: sessionId,
191
+ message_id: messageId,
192
+ },
193
+ select: { session_id: true, message_id: true },
194
+ orderBy: { started_at: 'desc' }
195
+ });
196
+ if (!run?.session_id || !run.message_id) {
197
+ res.json({
198
+ found: false,
199
+ logs: null
200
+ });
201
+ return;
202
+ }
203
+ const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
204
+ const workflowRunsDir = path_1.default.join(workspaceDir, 'workflow-runs');
205
+ const logPath = path_1.default.join(workflowRunsDir, `${sanitizeFilenamePart(run.session_id)}-${sanitizeFilenamePart(run.message_id)}.txt`);
206
+ if (!isPathInside(logPath, workflowRunsDir)) {
207
+ throw {
208
+ status: 400,
209
+ message: 'Invalid log path'
210
+ };
211
+ }
212
+ try {
213
+ const logs = await promises_1.default.readFile(logPath, 'utf-8');
214
+ res.json({
215
+ found: true,
216
+ logs
217
+ });
218
+ }
219
+ catch {
220
+ res.json({
221
+ found: false,
222
+ logs: null
223
+ });
224
+ }
225
+ }, true));
226
+ // GET /api/workflows/runs/:id - Get details for a specific workflow run
227
+ router.get('/runs/:id', (0, index_1.apiHandler)(async (req, res) => {
228
+ const id = req.params.id;
229
+ const run = await client_1.default.workflow_runs.findUnique({
230
+ where: { id },
231
+ include: {
232
+ user: {
233
+ select: { name: true, email: true }
234
+ }
235
+ }
236
+ });
237
+ if (!run) {
238
+ throw {
239
+ status: 404,
240
+ message: 'Workflow run not found'
241
+ };
242
+ }
243
+ res.json({ run });
244
+ }, true));
245
+ // GET /api/workflows/interruption-status/:sessionId - Check if a workflow session should stop
246
+ router.get('/interruption-status/:sessionId', (0, index_1.apiHandler)(async (req, res) => {
247
+ const sessionId = req.params.sessionId;
248
+ const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
249
+ const interrupted = await (0, workflow_interruption_1.isWorkflowSessionInterrupted)(sessionId, workspaceDir);
250
+ res.json({ interrupted });
251
+ }, true));
252
+ // POST /api/workflows/runs/:id/stop - Stop an in-progress workflow run
253
+ router.post('/runs/:id/stop', (0, index_1.apiHandler)(async (req, res) => {
254
+ const id = req.params.id;
255
+ const run = await client_1.default.workflow_runs.findUnique({
256
+ where: { id }
257
+ });
258
+ if (!run) {
259
+ throw {
260
+ status: 404,
261
+ message: 'Workflow run not found'
262
+ };
263
+ }
264
+ if (run.status !== 'running') {
265
+ res.json({ success: true });
266
+ return;
267
+ }
268
+ const permissionSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", run.workflow_slug, req.userId);
269
+ if (!permissionSummary.can_edit) {
270
+ throw {
271
+ status: 403,
272
+ message: 'You do not have permission to stop this workflow run'
273
+ };
274
+ }
275
+ if (!run.session_id) {
276
+ throw {
277
+ status: 404,
278
+ message: 'Workflow run session not found'
279
+ };
280
+ }
281
+ const isManualSession = run.session_id.startsWith("manual-");
282
+ if (isManualSession) {
283
+ await (0, workflow_interruption_1.markWorkflowSessionAborted)(run.session_id);
284
+ }
285
+ else {
286
+ await (0, session_abort_1.abortOpencodeSession)(run.session_id);
287
+ }
288
+ res.json({ success: true });
289
+ }, true));
290
+ // POST /api/workflows/:slug/manual-run - Start a workflow run from manual mode UI
291
+ router.post('/:slug/manual-run', (0, index_1.apiHandler)(async (req, res) => {
292
+ const slug = req.params.slug;
293
+ const inputsRaw = req.body.inputs ?? {};
294
+ if (!inputsRaw || typeof inputsRaw !== 'object' || Array.isArray(inputsRaw)) {
295
+ throw {
296
+ status: 400,
297
+ message: 'inputs must be an object'
298
+ };
299
+ }
300
+ const inputs = inputsRaw;
301
+ const manualSessionId = `manual-${req.userId}-${(0, crypto_1.randomUUID)()}`;
302
+ const manualMessageId = `manual-message-${(0, crypto_1.randomUUID)()}`;
303
+ const manualCallId = `manual-call-${(0, crypto_1.randomUUID)()}`;
304
+ const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
305
+ await assertCurrentUserCanRunWorkflow(slug, req.userId);
306
+ const startedRun = await (0, workflow_runner_1.startWorkflowRunViaBackend)({
307
+ workspaceDir,
308
+ slug,
309
+ inputs,
310
+ authUserId: req.userId,
311
+ sessionId: manualSessionId,
312
+ messageId: manualMessageId,
313
+ callId: manualCallId,
314
+ requirePermissionPrompt: false,
315
+ });
316
+ void startedRun.completion.catch(() => undefined);
317
+ res.json({
318
+ run_id: startedRun.runId
319
+ });
320
+ }, true));
321
+ // POST /api/workflows/execute - Start workflow execution without blocking on completion.
322
+ router.post('/execute', (0, index_1.apiHandler)(async (req, res) => {
323
+ if (!req.opencode_session_id) {
324
+ throw {
325
+ status: 400,
326
+ message: 'This endpoint requires an opencode session token'
327
+ };
328
+ }
329
+ const body = req.body;
330
+ if (typeof body.slug !== "string") {
331
+ throw { status: 400, message: "slug is required" };
332
+ }
333
+ if (typeof body.message_id !== "string") {
334
+ throw { status: 400, message: "message_id is required" };
335
+ }
336
+ if (typeof body.call_id !== "string") {
337
+ throw { status: 400, message: "call_id is required" };
338
+ }
339
+ const slug = body.slug;
340
+ const inputs = body.inputs ?? {};
341
+ const messageId = body.message_id;
342
+ const callId = body.call_id;
343
+ const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
344
+ await assertCurrentUserCanRunWorkflow(slug, req.userId);
345
+ const startedRun = await (0, workflow_runner_1.startWorkflowRunViaBackend)({
346
+ workspaceDir,
347
+ slug,
348
+ inputs,
349
+ authUserId: req.userId,
350
+ sessionId: req.opencode_session_id,
351
+ messageId,
352
+ callId,
353
+ requirePermissionPrompt: true,
354
+ });
355
+ const executionId = (0, crypto_1.randomUUID)();
356
+ const executionRecord = {
357
+ slug,
358
+ runId: startedRun.runId,
359
+ timeoutSeconds: startedRun.timeoutSeconds,
360
+ status: "running",
361
+ output: null,
362
+ errorMessage: null,
363
+ };
364
+ workflowExecutions.set(executionId, executionRecord);
365
+ void startedRun.completion
366
+ .then((result) => {
367
+ executionRecord.output = result.output;
368
+ executionRecord.status = result.status;
369
+ executionRecord.errorMessage = result.status === "success" ? null : `Workflow execution ${result.status}`;
370
+ })
371
+ .catch((err) => {
372
+ const errorOutput = err instanceof Error ? err.message : JSON.stringify(err);
373
+ executionRecord.status = "error";
374
+ executionRecord.errorMessage = errorOutput;
375
+ executionRecord.output = errorOutput;
376
+ });
377
+ res.json({
378
+ execution_id: executionId,
379
+ status: "running",
380
+ });
381
+ }, true));
382
+ // GET /api/workflows/execute/:executionId - Get execution status/result.
383
+ router.get('/execute/:executionId', (0, index_1.apiHandler)(async (req, res) => {
384
+ const executionId = req.params.executionId;
385
+ const execution = workflowExecutions.get(executionId);
386
+ if (!execution) {
387
+ throw {
388
+ status: 404,
389
+ message: 'Execution not found'
390
+ };
391
+ }
392
+ if (execution.status === "running") {
393
+ res.json({
394
+ execution_id: executionId,
395
+ status: "running",
396
+ });
397
+ return;
398
+ }
399
+ const responsePayload = {
400
+ status: execution.status,
401
+ output: execution.output,
402
+ workflow: execution.slug,
403
+ timeout_seconds: execution.timeoutSeconds,
404
+ run_id: execution.runId,
405
+ };
406
+ workflowExecutions.delete(executionId);
407
+ if (execution.status !== "success") {
408
+ throw {
409
+ status: 500,
410
+ message: "Workflow execution failed: " + JSON.stringify(responsePayload),
411
+ doLogging: false,
412
+ maskErrorMessage: false,
413
+ };
414
+ }
415
+ res.json(responsePayload);
416
+ }, true));
417
+ // PATCH /api/workflows/runs/:id - Update run status
418
+ router.patch('/runs/:id', (0, index_1.apiHandler)(async (req, res) => {
419
+ const id = req.params.id;
420
+ const { status, error_message, output } = req.body;
421
+ if (!status || !['running', 'success', 'failed'].includes(status)) {
422
+ throw {
423
+ status: 400,
424
+ message: 'status must be "running", "success", or "failed"'
425
+ };
426
+ }
427
+ const existingRun = await client_1.default.workflow_runs.findUnique({ where: { id } });
428
+ if (!existingRun) {
429
+ throw {
430
+ status: 404,
431
+ message: 'Workflow run not found'
432
+ };
433
+ }
434
+ if (existingRun.status !== 'running') {
435
+ throw {
436
+ status: 400,
437
+ message: 'Can only update runs that are in running status'
438
+ };
439
+ }
440
+ const updateData = { status };
441
+ if (status === 'success' || status === 'failed') {
442
+ updateData.completed_at = BigInt(Date.now());
443
+ }
444
+ if (status === 'failed' && error_message) {
445
+ updateData.error_message = error_message;
446
+ }
447
+ if (output) {
448
+ updateData.output = output;
449
+ }
450
+ const run = await client_1.default.workflow_runs.update({
451
+ where: { id },
452
+ data: updateData
453
+ });
454
+ res.json({ run });
455
+ }, true));
456
+ // POST /api/workflows/:slug/approve - Approve a workflow
457
+ router.post('/:slug/approve', (0, index_1.apiHandler)(async (req, res) => {
458
+ const slug = req.params.slug;
459
+ const approvalResult = await (0, workflow_approval_snapshot_1.approveWorkflowWithSnapshot)(slug, req.userId);
460
+ const { metadata: approvedMetadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
461
+ await (0, workflow_permissions_1.addApproverToWorkflowRunPermissionsIfRestricted)(slug, req.userId, approvedMetadata.created_by_user_id);
462
+ res.json({
463
+ workflow: {
464
+ slug,
465
+ approved_by_user_id: approvalResult.approved_by_user_id,
466
+ is_approved: true,
467
+ snapshot_hash: approvalResult.snapshot_hash,
468
+ snapshot_file_count: approvalResult.snapshot_file_count
469
+ }
470
+ });
471
+ }, true));
472
+ // POST /api/workflows/:slug/reject-restore - Restore workflow files to last approved snapshot
473
+ router.post('/:slug/reject-restore', (0, index_1.apiHandler)(async (req, res) => {
474
+ const slug = req.params.slug;
475
+ const result = await (0, workflow_approval_snapshot_1.restoreWorkflowToApprovedSnapshot)(slug, req.userId);
476
+ res.json({
477
+ workflow: {
478
+ slug,
479
+ restored_file_count: result.restored_file_count,
480
+ snapshot_hash: result.snapshot_hash,
481
+ }
482
+ });
483
+ }, true));
484
+ // POST /api/workflows/:slug/creator - Set the creator user id for a workflow
485
+ router.post('/:slug/creator', (0, index_1.apiHandler)(async (req, res) => {
486
+ const slug = req.params.slug;
487
+ const { metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
488
+ const existingCreator = metadata.created_by_user_id ?? null;
489
+ if (existingCreator && existingCreator !== req.userId) {
490
+ throw {
491
+ status: 409,
492
+ message: 'Workflow creator is already set and cannot be changed'
493
+ };
494
+ }
495
+ const updatedMetadata = existingCreator
496
+ ? metadata
497
+ : await (0, workflow_1.setWorkflowCreator)(slug, req.userId);
498
+ await (0, workflow_permissions_1.initializeWorkflowRunPermissionsForCreator)(slug, req.userId);
499
+ res.json({
500
+ workflow: {
501
+ slug,
502
+ created_by_user_id: updatedMetadata.created_by_user_id
503
+ }
504
+ });
505
+ }, true));
506
+ // DELETE /api/workflows/:slug - Delete workflow directory and all past runs
507
+ router.delete('/:slug', (0, index_1.apiHandler)(async (req, res) => {
508
+ const slug = req.params.slug;
509
+ const { metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
510
+ const creatorUserId = metadata.created_by_user_id ?? null;
511
+ const creator = creatorUserId
512
+ ? await client_1.default.users.findUnique({
513
+ where: { id: creatorUserId },
514
+ select: { id: true }
515
+ })
516
+ : null;
517
+ const isOwner = creatorUserId === req.userId;
518
+ const hasNoCreatorUser = creator === null;
519
+ const isEngineer = req.role === 'Engineer';
520
+ if (!isOwner && !(hasNoCreatorUser && isEngineer)) {
521
+ throw {
522
+ status: 403,
523
+ message: 'Only the workflow owner can delete this workflow. Engineers can only delete workflows whose owner no longer exists.'
524
+ };
525
+ }
526
+ await (0, workflow_1.deleteWorkflow)(slug);
527
+ res.json({ success: true });
528
+ }, true));
529
+ // GET /api/workflows/:slug/approval-diff - Preview current code diff vs approved snapshot
530
+ router.get('/:slug/approval-diff', (0, index_1.apiHandler)(async (req, res) => {
531
+ const slug = req.params.slug;
532
+ if (req.role !== 'Engineer') {
533
+ throw {
534
+ status: 403,
535
+ message: 'Only Engineers can review approval diffs'
536
+ };
537
+ }
538
+ await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
539
+ const previousSnapshot = await (0, workflow_approval_snapshot_1.loadApprovedSnapshotFromDb)(slug);
540
+ const currentSnapshot = (0, workflow_approval_snapshot_1.collectCurrentWorkflowSnapshot)(slug);
541
+ const diff = (0, workflow_approval_snapshot_1.buildApprovalDiffResponse)(previousSnapshot, currentSnapshot);
542
+ res.locals.skipResponseSanitization = true;
543
+ res.json(diff);
544
+ }, true));
545
+ (0, resource_file_routes_1.registerResourceFileRoutes)({
546
+ router,
547
+ uploadMiddleware: workflowFileUpload.single("file"),
548
+ assertCanView: assertCanViewWorkflowFiles,
549
+ ensureResourceExists: async (slug) => {
550
+ await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
551
+ },
552
+ getEditorAccess: getWorkflowEditorAccess,
553
+ assertCanEdit: assertCanEditWorkflowFiles,
554
+ listDirectory: workflow_files_1.listWorkflowDirectory,
555
+ readFileContent: workflow_files_1.readWorkflowFileContent,
556
+ saveFileContent: workflow_files_1.saveWorkflowFileContent,
557
+ createFileOrFolder: workflow_files_1.createWorkflowFileOrFolder,
558
+ uploadFileFromTempPath: workflow_files_1.uploadWorkflowFileFromTempPath,
559
+ renamePath: workflow_files_1.renameWorkflowPath,
560
+ deletePath: workflow_files_1.deleteWorkflowPath,
561
+ });
562
+ // GET /api/workflows/:slug - Get workflow details from filesystem
563
+ router.get('/:slug', (0, index_1.apiHandler)(async (req, res) => {
564
+ const slug = req.params.slug;
565
+ await assertCanViewWorkflowFiles(slug, req.userId);
566
+ const { manifest, metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
567
+ const permission = await (0, workflow_permissions_1.getWorkflowRunPermissionWithUsers)(slug);
568
+ const accessSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, req.userId);
569
+ const createdByUserId = metadata.created_by_user_id ?? null;
570
+ const approvedByUserId = metadata.approved_by_user_id ?? null;
571
+ const userIds = [createdByUserId, approvedByUserId].filter((id) => typeof id === "string");
572
+ const users = userIds.length > 0
573
+ ? await client_1.default.users.findMany({
574
+ where: { id: { in: userIds } },
575
+ select: { id: true, name: true, email: true }
576
+ })
577
+ : [];
578
+ const usersById = new Map(users.map((user) => [user.id, user]));
579
+ const creator = createdByUserId ? (usersById.get(createdByUserId) ?? null) : null;
580
+ const approver = approvedByUserId ? (usersById.get(approvedByUserId) ?? null) : null;
581
+ const permissionMode = (0, permission_common_1.assertCommonPermissionMode)(permission.permission_mode, "workflow run");
582
+ const permissions = permissionMode === "everyone"
583
+ ? { mode: "everyone" }
584
+ : { mode: "restricted", allowed_user_ids: permission.allowedUsers.map((row) => row.user_id) };
585
+ res.json({
586
+ workflow: {
587
+ slug,
588
+ name: slug,
589
+ intent_summary: manifest.intent_summary,
590
+ created_by_user_id: createdByUserId,
591
+ created_by_user_name: creator?.name ?? null,
592
+ created_by_user_email: creator?.email ?? null,
593
+ approved_by_user_id: approvedByUserId,
594
+ is_approved: accessSummary.is_approved,
595
+ approved_by_user_name: approver?.name ?? null,
596
+ approved_by_user_email: approver?.email ?? null,
597
+ can_view: accessSummary.can_view,
598
+ can_edit: accessSummary.can_edit,
599
+ permission_mode: accessSummary.permission_mode,
600
+ is_locked_due_to_missing_users: accessSummary.is_locked_due_to_missing_users,
601
+ permissions,
602
+ allowed_users_resolved: permission.allowedUsers.map((row) => ({
603
+ user_id: row.user.id,
604
+ name: row.user.name,
605
+ email: row.user.email,
606
+ is_owner: row.user.id === metadata.created_by_user_id,
607
+ is_approver: row.user.id === metadata.approved_by_user_id
608
+ })),
609
+ manifest
610
+ }
611
+ });
612
+ }, true));
613
+ const updateWorkflowPermissionsHandler = (0, index_1.apiHandler)(async (req, res) => {
614
+ const slug = req.params.slug;
615
+ const { metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
616
+ const approvalState = await (0, workflow_approval_snapshot_1.getWorkflowSnapshotApprovalState)(slug);
617
+ if (!approvalState.is_current_code_approved) {
618
+ throw {
619
+ status: 403,
620
+ message: 'Workflow must be approved before updating run permissions'
621
+ };
622
+ }
623
+ const currentSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, req.userId);
624
+ if (!currentSummary.can_edit) {
625
+ throw {
626
+ status: 403,
627
+ message: currentSummary.is_locked_due_to_missing_users
628
+ ? 'Workflow permissions cannot be modified because no allowed users remain'
629
+ : 'You do not have permission to modify workflow run permissions'
630
+ };
631
+ }
632
+ const { mode, allowed_user_ids } = req.body;
633
+ if (mode !== 'restricted' && mode !== 'everyone') {
634
+ throw {
635
+ status: 400,
636
+ message: 'mode must be "restricted" or "everyone"'
637
+ };
638
+ }
639
+ const updatedPermission = mode === 'everyone'
640
+ ? await (0, workflow_permissions_1.setWorkflowRunPermissions)(slug, { mode: 'everyone' }, metadata.created_by_user_id)
641
+ : await (0, workflow_permissions_1.setWorkflowRunPermissions)(slug, {
642
+ mode: 'restricted',
643
+ allowed_user_ids: Array.isArray(allowed_user_ids) ? allowed_user_ids.map((id) => String(id)) : []
644
+ }, metadata.created_by_user_id);
645
+ const updatedSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, req.userId);
646
+ const updatedPermissionMode = (0, permission_common_1.assertCommonPermissionMode)(updatedPermission.permission_mode, "workflow run");
647
+ const permissions = updatedPermissionMode === "everyone"
648
+ ? { mode: "everyone" }
649
+ : { mode: "restricted", allowed_user_ids: updatedPermission.allowedUsers.map((row) => row.user_id) };
650
+ res.json({
651
+ workflow: {
652
+ slug,
653
+ ...updatedSummary,
654
+ permissions,
655
+ allowed_users_resolved: updatedPermission.allowedUsers.map((row) => ({
656
+ user_id: row.user.id,
657
+ name: row.user.name,
658
+ email: row.user.email,
659
+ is_owner: row.user.id === metadata.created_by_user_id,
660
+ is_approver: row.user.id === metadata.approved_by_user_id
661
+ }))
662
+ }
663
+ });
664
+ }, true);
665
+ // PATCH /api/workflows/:slug/permissions - Update permissions (canonical)
666
+ router.patch('/:slug/permissions', updateWorkflowPermissionsHandler);
667
+ // POST /api/workflows/request-permission - Create a tool execution permission request
668
+ router.post('/request-permission', (0, index_1.apiHandler)(async (req, res) => {
669
+ const { message_id, call_id } = req.body;
670
+ if (!message_id || !call_id) {
671
+ throw {
672
+ status: 400,
673
+ message: 'opencode_session_id, message_id, and call_id are required'
674
+ };
675
+ }
676
+ // Create or update permission request in database
677
+ const permission = await client_1.default.tool_execution_permissions.upsert({
678
+ where: {
679
+ opencode_session_id_message_id_call_id: {
680
+ opencode_session_id: req.opencode_session_id,
681
+ message_id,
682
+ call_id
683
+ }
684
+ },
685
+ create: {
686
+ opencode_session_id: req.opencode_session_id,
687
+ message_id,
688
+ call_id,
689
+ status: 'pending',
690
+ created_at: BigInt(Date.now())
691
+ },
692
+ update: {
693
+ status: 'pending',
694
+ responded_at: null
695
+ }
696
+ });
697
+ res.json({ permission_id: permission.id });
698
+ }, true)); // Plugin authenticates with opencode session token
699
+ // GET /api/workflows/permission-status/:id - Check permission status
700
+ router.get('/permission-status/:id', (0, index_1.apiHandler)(async (req, res) => {
701
+ const id = req.params.id;
702
+ const permission = await client_1.default.tool_execution_permissions.findUnique({
703
+ where: { id }
704
+ });
705
+ if (!permission) {
706
+ throw {
707
+ status: 404,
708
+ message: 'Permission request not found'
709
+ };
710
+ }
711
+ if (req.opencode_session_id !== permission.opencode_session_id) {
712
+ throw {
713
+ status: 403,
714
+ message: 'Authorization token does not match permission session'
715
+ };
716
+ }
717
+ res.json({
718
+ status: permission.status,
719
+ approved: permission.status === 'approved'
720
+ });
721
+ }, true)); // Plugin authenticates with opencode session token
722
+ // POST /api/workflows/permission-reject/:id - Reject a pending permission request (plugin cleanup)
723
+ router.post('/permission-reject/:id', (0, index_1.apiHandler)(async (req, res) => {
724
+ const id = req.params.id;
725
+ const permission = await client_1.default.tool_execution_permissions.findUnique({
726
+ where: { id }
727
+ });
728
+ if (!permission) {
729
+ throw {
730
+ status: 404,
731
+ message: 'Permission request not found'
732
+ };
733
+ }
734
+ if (req.opencode_session_id !== permission.opencode_session_id) {
735
+ throw {
736
+ status: 403,
737
+ message: 'Authorization token does not match permission session'
738
+ };
739
+ }
740
+ if (permission.status === 'pending') {
741
+ await client_1.default.tool_execution_permissions.update({
742
+ where: { id },
743
+ data: {
744
+ status: 'rejected',
745
+ responded_at: BigInt(Date.now())
746
+ }
747
+ });
748
+ }
749
+ res.json({ success: true });
750
+ }, true)); // Plugin authenticates with opencode session token
751
+ exports.default = router;