teamcopilot 0.3.3 → 0.3.5

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 (40) hide show
  1. package/.env.example +3 -0
  2. package/dist/frontend/assets/{cssMode-DXQJFMVx.js → cssMode-DUnSc-N1.js} +1 -1
  3. package/dist/frontend/assets/{freemarker2-BW1EZI_f.js → freemarker2-C_BuPYAu.js} +1 -1
  4. package/dist/frontend/assets/{handlebars-Biheo7KR.js → handlebars-DEEie70Y.js} +1 -1
  5. package/dist/frontend/assets/{html-BH01Jifq.js → html-BFks4r5R.js} +1 -1
  6. package/dist/frontend/assets/{htmlMode-BhB6cjl6.js → htmlMode-D8q9uv51.js} +1 -1
  7. package/dist/frontend/assets/index-D1Hcz_bo.css +1 -0
  8. package/dist/frontend/assets/{index-Dl290_nP.js → index-DSsnnTDn.js} +224 -219
  9. package/dist/frontend/assets/{javascript-vRQ7Z-SD.js → javascript-LbJP7XES.js} +1 -1
  10. package/dist/frontend/assets/{jsonMode-CQXvACVS.js → jsonMode-CTDSTkzw.js} +1 -1
  11. package/dist/frontend/assets/{liquid-H0P3dWK2.js → liquid-DMVVOmiW.js} +1 -1
  12. package/dist/frontend/assets/{mdx-Bz0q5Fh9.js → mdx-CG2zKU-F.js} +1 -1
  13. package/dist/frontend/assets/{python-v4fpwY3A.js → python-DupB8Ozc.js} +1 -1
  14. package/dist/frontend/assets/{razor-ptFSsABU.js → razor-DNRGk0B3.js} +1 -1
  15. package/dist/frontend/assets/{tsMode-DgGhJJ5C.js → tsMode-cWXCeSel.js} +1 -1
  16. package/dist/frontend/assets/{typescript-C7Kjd36G.js → typescript-D_Aw0wph.js} +1 -1
  17. package/dist/frontend/assets/{xml-BBvPtCBs.js → xml-DPt8oeKw.js} +1 -1
  18. package/dist/frontend/assets/{yaml-dCpIE0um.js → yaml-Ct0xMYlA.js} +1 -1
  19. package/dist/frontend/index.html +2 -2
  20. package/dist/index.js +2 -0
  21. package/dist/utils/external-host.js +22 -0
  22. package/dist/utils/secrets.js +15 -0
  23. package/dist/utils/workflow-api-keys.js +88 -0
  24. package/dist/utils/workflow-interruption.js +2 -2
  25. package/dist/utils/workflow-runner.js +11 -2
  26. package/dist/utils/workflow.js +3 -0
  27. package/dist/workflow-api.js +185 -0
  28. package/dist/workflows/index.js +35 -0
  29. package/package.json +1 -1
  30. package/prisma/generated/client/edge.js +15 -4
  31. package/prisma/generated/client/index-browser.js +12 -1
  32. package/prisma/generated/client/index.d.ts +2002 -270
  33. package/prisma/generated/client/index.js +15 -4
  34. package/prisma/generated/client/package.json +1 -1
  35. package/prisma/generated/client/schema.prisma +28 -12
  36. package/prisma/generated/client/wasm.js +15 -4
  37. package/prisma/migrations/20260430033133_add_workflow_api_keys/migration.sql +46 -0
  38. package/prisma/migrations/20260430161041_remove_run_source_default/migration.sql +28 -0
  39. package/prisma/schema.prisma +18 -2
  40. package/dist/frontend/assets/index-CaegH8GM.css +0 -1
@@ -0,0 +1,88 @@
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
+ exports.assertCanManageWorkflowApiKeys = assertCanManageWorkflowApiKeys;
7
+ exports.listWorkflowApiKeys = listWorkflowApiKeys;
8
+ exports.createWorkflowApiKey = createWorkflowApiKey;
9
+ exports.deleteWorkflowApiKey = deleteWorkflowApiKey;
10
+ const crypto_1 = require("crypto");
11
+ const client_1 = __importDefault(require("../prisma/client"));
12
+ const resource_access_1 = require("./resource-access");
13
+ const workflow_approval_snapshot_1 = require("./workflow-approval-snapshot");
14
+ const workflow_1 = require("./workflow");
15
+ async function assertCanManageWorkflowApiKeys(slug, userId) {
16
+ await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
17
+ const approvalState = await (0, workflow_approval_snapshot_1.getWorkflowSnapshotApprovalState)(slug);
18
+ if (!approvalState.is_current_code_approved) {
19
+ throw {
20
+ status: 403,
21
+ message: "Workflow must be approved before managing API keys"
22
+ };
23
+ }
24
+ const access = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, userId);
25
+ if (!access.can_edit) {
26
+ throw {
27
+ status: 403,
28
+ message: "You do not have permission to manage API keys for this workflow"
29
+ };
30
+ }
31
+ }
32
+ async function ensureWorkflowApiKey(slug, createdByUserId) {
33
+ const existing = await client_1.default.workflow_api_keys.findFirst({
34
+ where: { workflow_slug: slug },
35
+ orderBy: { created_at: "asc" }
36
+ });
37
+ if (existing) {
38
+ return existing;
39
+ }
40
+ return await client_1.default.workflow_api_keys.create({
41
+ data: {
42
+ workflow_slug: slug,
43
+ api_key: (0, crypto_1.randomUUID)(),
44
+ created_by_user_id: createdByUserId,
45
+ created_at: BigInt(Date.now()),
46
+ }
47
+ });
48
+ }
49
+ async function listWorkflowApiKeys(slug, createdByUserId) {
50
+ await ensureWorkflowApiKey(slug, createdByUserId);
51
+ return await client_1.default.workflow_api_keys.findMany({
52
+ where: { workflow_slug: slug },
53
+ orderBy: { created_at: "asc" }
54
+ });
55
+ }
56
+ async function createWorkflowApiKey(slug, createdByUserId) {
57
+ return await client_1.default.workflow_api_keys.create({
58
+ data: {
59
+ workflow_slug: slug,
60
+ api_key: (0, crypto_1.randomUUID)(),
61
+ created_by_user_id: createdByUserId,
62
+ created_at: BigInt(Date.now()),
63
+ }
64
+ });
65
+ }
66
+ async function deleteWorkflowApiKey(slug, keyId) {
67
+ const key = await client_1.default.workflow_api_keys.findUnique({
68
+ where: { id: keyId }
69
+ });
70
+ if (!key || key.workflow_slug !== slug) {
71
+ throw {
72
+ status: 404,
73
+ message: "Workflow API key not found"
74
+ };
75
+ }
76
+ const keyCount = await client_1.default.workflow_api_keys.count({
77
+ where: { workflow_slug: slug }
78
+ });
79
+ if (keyCount <= 1) {
80
+ throw {
81
+ status: 400,
82
+ message: "Each workflow must always have at least one API key"
83
+ };
84
+ }
85
+ await client_1.default.workflow_api_keys.delete({
86
+ where: { id: keyId }
87
+ });
88
+ }
@@ -8,8 +8,8 @@ exports.markWorkflowSessionAborted = markWorkflowSessionAborted;
8
8
  const client_1 = __importDefault(require("../prisma/client"));
9
9
  const opencode_client_1 = require("./opencode-client");
10
10
  async function isWorkflowSessionInterrupted(sessionId, workspaceDir) {
11
- const isManualSession = sessionId.startsWith("manual-");
12
- if (isManualSession) {
11
+ const usesDatabaseAbortMarker = sessionId.startsWith("manual-") || sessionId.startsWith("api-");
12
+ if (usesDatabaseAbortMarker) {
13
13
  const aborted = await client_1.default.workflow_aborted_sessions.findUnique({
14
14
  where: { session_id: sessionId }
15
15
  });
@@ -382,9 +382,16 @@ async function startWorkflowRunViaBackend(options) {
382
382
  if (!validation.valid) {
383
383
  throw new Error(`Input validation failed: ${JSON.stringify(validation.errors)}`);
384
384
  }
385
- const secretResolution = await (0, secrets_1.resolveSecretsForUser)(options.authUserId, requiredSecrets);
385
+ const secretResolutionMode = options.secretResolutionMode;
386
+ if (secretResolutionMode === "user" && !options.authUserId) {
387
+ throw new Error("authUserId is required for user secret resolution.");
388
+ }
389
+ const secretResolution = secretResolutionMode === "global"
390
+ ? await (0, secrets_1.resolveGlobalSecrets)(requiredSecrets)
391
+ : await (0, secrets_1.resolveSecretsForUser)(options.authUserId, requiredSecrets);
386
392
  if (secretResolution.missingKeys.length > 0) {
387
- throw new Error(`Missing required secrets: ${secretResolution.missingKeys.join(", ")}. Add these keys in your profile secrets before running this workflow.`);
393
+ const secretLocation = secretResolutionMode === "global" ? "global secrets" : "your profile secrets";
394
+ throw new Error(`Missing required secrets: ${secretResolution.missingKeys.join(", ")}. Add these keys in ${secretLocation} before running this workflow.`);
388
395
  }
389
396
  const timeoutSeconds = parseTimeoutSeconds(workflowJson.runtime?.timeout_seconds);
390
397
  if (!timeoutSeconds) {
@@ -400,6 +407,8 @@ async function startWorkflowRunViaBackend(options) {
400
407
  args: JSON.stringify(options.inputs),
401
408
  session_id: options.sessionId,
402
409
  message_id: options.messageId,
410
+ run_source: options.runSource,
411
+ workflow_api_key_id: options.workflowApiKeyId ?? null,
403
412
  }
404
413
  });
405
414
  const workflowRunsDir = path.join(options.workspaceDir, "workflow-runs");
@@ -45,6 +45,9 @@ async function deleteWorkflow(slug) {
45
45
  await client_1.default.workflow_runs.deleteMany({
46
46
  where: { workflow_slug: slug }
47
47
  });
48
+ await client_1.default.workflow_api_keys.deleteMany({
49
+ where: { workflow_slug: slug }
50
+ });
48
51
  await client_1.default.resource_metadata.deleteMany({
49
52
  where: {
50
53
  resource_kind: "workflow",
@@ -0,0 +1,185 @@
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 promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const crypto_1 = require("crypto");
10
+ const client_1 = __importDefault(require("./prisma/client"));
11
+ const workflow_1 = require("./utils/workflow");
12
+ const workflow_approval_snapshot_1 = require("./utils/workflow-approval-snapshot");
13
+ const workspace_sync_1 = require("./utils/workspace-sync");
14
+ const workflow_runner_1 = require("./utils/workflow-runner");
15
+ const workflow_interruption_1 = require("./utils/workflow-interruption");
16
+ function sanitizeFilenamePart(value) {
17
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
18
+ }
19
+ function isPathInside(childPath, parentPath) {
20
+ const parent = path_1.default.resolve(parentPath) + path_1.default.sep;
21
+ const child = path_1.default.resolve(childPath) + path_1.default.sep;
22
+ return child.startsWith(parent);
23
+ }
24
+ function workflowApiHandler(handler) {
25
+ return async (req, res, next) => {
26
+ try {
27
+ const authHeader = req.headers.authorization;
28
+ if (!authHeader) {
29
+ throw {
30
+ status: 401,
31
+ message: "Missing authorization header. Please pass the workflow API key as an authorization bearer token."
32
+ };
33
+ }
34
+ const rawToken = authHeader.split(" ")[1];
35
+ if (!rawToken) {
36
+ throw {
37
+ status: 401,
38
+ message: "Missing authorization bearer token"
39
+ };
40
+ }
41
+ const key = await client_1.default.workflow_api_keys.findUnique({
42
+ where: { api_key: rawToken },
43
+ select: { id: true, workflow_slug: true, api_key: true }
44
+ });
45
+ if (!key) {
46
+ throw {
47
+ status: 401,
48
+ message: "Invalid workflow API key"
49
+ };
50
+ }
51
+ const apiReq = req;
52
+ apiReq.workflowApiKey = key;
53
+ await handler(apiReq, res);
54
+ }
55
+ catch (err) {
56
+ next(err);
57
+ }
58
+ };
59
+ }
60
+ async function assertWorkflowCanRunViaApi(slug) {
61
+ await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
62
+ const approvalState = await (0, workflow_approval_snapshot_1.getWorkflowSnapshotApprovalState)(slug);
63
+ if (!approvalState.is_current_code_approved) {
64
+ throw {
65
+ status: 403,
66
+ message: "Workflow is not approved for the current code version"
67
+ };
68
+ }
69
+ }
70
+ async function assertApiKeyCanAccessRun(req, runHandle) {
71
+ const run = await client_1.default.workflow_runs.findUnique({
72
+ where: { id: runHandle }
73
+ });
74
+ if (!run) {
75
+ throw {
76
+ status: 404,
77
+ message: "Workflow run not found"
78
+ };
79
+ }
80
+ if (req.workflowApiKey.workflow_slug !== run.workflow_slug) {
81
+ throw {
82
+ status: 403,
83
+ message: "Workflow API key does not have access to this run"
84
+ };
85
+ }
86
+ return run;
87
+ }
88
+ async function readWorkflowRunLogs(run) {
89
+ if (!run.session_id || !run.message_id) {
90
+ return null;
91
+ }
92
+ const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
93
+ const workflowRunsDir = path_1.default.join(workspaceDir, "workflow-runs");
94
+ const logPath = path_1.default.join(workflowRunsDir, `${sanitizeFilenamePart(run.session_id)}-${sanitizeFilenamePart(run.message_id)}.txt`);
95
+ if (!isPathInside(logPath, workflowRunsDir)) {
96
+ throw {
97
+ status: 400,
98
+ message: "Invalid log path"
99
+ };
100
+ }
101
+ try {
102
+ return await promises_1.default.readFile(logPath, "utf-8");
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ function parseRunInputs(args) {
109
+ if (args === null || args === "") {
110
+ return {};
111
+ }
112
+ return JSON.parse(args);
113
+ }
114
+ const workflowApiRouter = express_1.default.Router();
115
+ workflowApiRouter.post("/runs", workflowApiHandler(async (req, res) => {
116
+ const body = req.body;
117
+ if (typeof body.workflow_slug !== "string" || body.workflow_slug.trim().length === 0) {
118
+ throw {
119
+ status: 400,
120
+ message: "workflow_slug is required"
121
+ };
122
+ }
123
+ if (body.inputs !== undefined && (!body.inputs || typeof body.inputs !== "object" || Array.isArray(body.inputs))) {
124
+ throw {
125
+ status: 400,
126
+ message: "inputs must be an object"
127
+ };
128
+ }
129
+ const slug = body.workflow_slug;
130
+ if (req.workflowApiKey.workflow_slug !== slug) {
131
+ throw {
132
+ status: 403,
133
+ message: "Workflow API key does not belong to this workflow"
134
+ };
135
+ }
136
+ await assertWorkflowCanRunViaApi(slug);
137
+ const startedRun = await (0, workflow_runner_1.startWorkflowRunViaBackend)({
138
+ workspaceDir: (0, workspace_sync_1.getWorkspaceDirFromEnv)(),
139
+ slug,
140
+ inputs: (body.inputs ?? {}),
141
+ authUserId: null,
142
+ sessionId: `api-${req.workflowApiKey.id}-${(0, crypto_1.randomUUID)()}`,
143
+ messageId: `api-message-${(0, crypto_1.randomUUID)()}`,
144
+ callId: `api-call-${(0, crypto_1.randomUUID)()}`,
145
+ requirePermissionPrompt: false,
146
+ secretResolutionMode: "global",
147
+ runSource: "api",
148
+ workflowApiKeyId: req.workflowApiKey.id,
149
+ });
150
+ void startedRun.completion.catch(() => undefined);
151
+ res.json({
152
+ run_handle: startedRun.runId
153
+ });
154
+ }));
155
+ workflowApiRouter.get("/runs/:runHandle", workflowApiHandler(async (req, res) => {
156
+ const runHandle = req.params.runHandle;
157
+ const run = await assertApiKeyCanAccessRun(req, runHandle);
158
+ const logs = await readWorkflowRunLogs(run);
159
+ const inputs = parseRunInputs(run.args);
160
+ res.json({
161
+ run_handle: run.id,
162
+ workflow_slug: run.workflow_slug,
163
+ status: run.status,
164
+ logs,
165
+ error_message: run.error_message,
166
+ started_at: run.started_at,
167
+ completed_at: run.completed_at,
168
+ inputs,
169
+ });
170
+ }));
171
+ workflowApiRouter.post("/runs/:runHandle/stop", workflowApiHandler(async (req, res) => {
172
+ const runHandle = req.params.runHandle;
173
+ const run = await assertApiKeyCanAccessRun(req, runHandle);
174
+ if (run.status === "running") {
175
+ if (!run.session_id) {
176
+ throw {
177
+ status: 404,
178
+ message: "Workflow run session not found"
179
+ };
180
+ }
181
+ await (0, workflow_interruption_1.markWorkflowSessionAborted)(run.session_id);
182
+ }
183
+ res.json({ success: true });
184
+ }));
185
+ exports.default = workflowApiRouter;
@@ -25,6 +25,8 @@ const resource_file_routes_1 = require("../utils/resource-file-routes");
25
25
  const resource_access_1 = require("../utils/resource-access");
26
26
  const secrets_1 = require("../utils/secrets");
27
27
  const secret_contract_validation_1 = require("../utils/secret-contract-validation");
28
+ const workflow_api_keys_1 = require("../utils/workflow-api-keys");
29
+ const external_host_1 = require("../utils/external-host");
28
30
  const router = express_1.default.Router({ mergeParams: true });
29
31
  const uploadTmpDir = path_1.default.join(os_1.default.tmpdir(), "teamcopilot-workflow-uploads");
30
32
  fs_1.default.mkdirSync(uploadTmpDir, { recursive: true });
@@ -317,6 +319,8 @@ router.post('/:slug/manual-run', (0, index_1.apiHandler)(async (req, res) => {
317
319
  messageId: manualMessageId,
318
320
  callId: manualCallId,
319
321
  requirePermissionPrompt: false,
322
+ runSource: "user",
323
+ secretResolutionMode: "user",
320
324
  });
321
325
  void startedRun.completion.catch(() => undefined);
322
326
  res.json({
@@ -356,6 +360,8 @@ router.post('/execute', (0, index_1.apiHandler)(async (req, res) => {
356
360
  messageId,
357
361
  callId,
358
362
  requirePermissionPrompt: true,
363
+ runSource: "user",
364
+ secretResolutionMode: "user",
359
365
  });
360
366
  const executionId = (0, crypto_1.randomUUID)();
361
367
  const executionRecord = {
@@ -532,6 +538,35 @@ router.delete('/:slug', (0, index_1.apiHandler)(async (req, res) => {
532
538
  await (0, workflow_1.deleteWorkflow)(slug);
533
539
  res.json({ success: true });
534
540
  }, true));
541
+ // GET /api/workflows/:slug/api-keys - List API keys for an approved workflow
542
+ router.get('/:slug/api-keys', (0, index_1.apiHandler)(async (req, res) => {
543
+ const slug = req.params.slug;
544
+ await (0, workflow_api_keys_1.assertCanManageWorkflowApiKeys)(slug, req.userId);
545
+ const apiKeys = await (0, workflow_api_keys_1.listWorkflowApiKeys)(slug, req.userId);
546
+ res.locals.skipResponseSanitization = true;
547
+ res.json({
548
+ api_base_url: (0, external_host_1.getWorkflowApiBaseUrl)(),
549
+ api_keys: apiKeys,
550
+ });
551
+ }, true));
552
+ // POST /api/workflows/:slug/api-keys - Add an API key for an approved workflow
553
+ router.post('/:slug/api-keys', (0, index_1.apiHandler)(async (req, res) => {
554
+ const slug = req.params.slug;
555
+ await (0, workflow_api_keys_1.assertCanManageWorkflowApiKeys)(slug, req.userId);
556
+ const apiKey = await (0, workflow_api_keys_1.createWorkflowApiKey)(slug, req.userId);
557
+ res.locals.skipResponseSanitization = true;
558
+ res.json({
559
+ api_key: apiKey,
560
+ });
561
+ }, true));
562
+ // DELETE /api/workflows/:slug/api-keys/:keyId - Remove an API key
563
+ router.delete('/:slug/api-keys/:keyId', (0, index_1.apiHandler)(async (req, res) => {
564
+ const slug = req.params.slug;
565
+ const keyId = req.params.keyId;
566
+ await (0, workflow_api_keys_1.assertCanManageWorkflowApiKeys)(slug, req.userId);
567
+ await (0, workflow_api_keys_1.deleteWorkflowApiKey)(slug, keyId);
568
+ res.json({ success: true });
569
+ }, true));
535
570
  // GET /api/workflows/:slug/approval-diff - Preview current code diff vs approved snapshot
536
571
  router.get('/:slug/approval-diff', (0, index_1.apiHandler)(async (req, res) => {
537
572
  const slug = req.params.slug;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamcopilot",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "A shared AI Agent for Teams",
5
5
  "homepage": "https://teamcopilot.ai",
6
6
  "repository": {