teamcopilot 0.1.16 → 0.2.0

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 (54) hide show
  1. package/README.md +88 -9
  2. package/dist/chat/index.js +23 -1
  3. package/dist/frontend/assets/{cssMode-CH26ItO2.js → cssMode-CM1GmZ3H.js} +1 -1
  4. package/dist/frontend/assets/{freemarker2-CiRHXG8W.js → freemarker2-C8TeljYR.js} +1 -1
  5. package/dist/frontend/assets/{handlebars-DXV-JQiR.js → handlebars-B2e-Wzyt.js} +1 -1
  6. package/dist/frontend/assets/{html-DKdYDRJv.js → html-DtBAvTj2.js} +1 -1
  7. package/dist/frontend/assets/{htmlMode-D466XPJJ.js → htmlMode-Dta08RE6.js} +1 -1
  8. package/dist/frontend/assets/index-BirlyHV4.css +1 -0
  9. package/dist/frontend/assets/{index-CvsPLefz.js → index-Dp0jlIX9.js} +201 -201
  10. package/dist/frontend/assets/{javascript-D5lHN8tF.js → javascript-BYeHq-2v.js} +1 -1
  11. package/dist/frontend/assets/{jsonMode-C9Wdxaho.js → jsonMode-DkJo6l8K.js} +1 -1
  12. package/dist/frontend/assets/{liquid-NIH--tpJ.js → liquid-nmEuajdb.js} +1 -1
  13. package/dist/frontend/assets/{mdx-xwEbqXME.js → mdx-BJybRyf3.js} +1 -1
  14. package/dist/frontend/assets/{python-BzErW_b3.js → python-DRAABm9s.js} +1 -1
  15. package/dist/frontend/assets/{razor-B0v-Bw5B.js → razor-7lH4jzk8.js} +1 -1
  16. package/dist/frontend/assets/{tsMode-B9YN5EEb.js → tsMode-ClcmdG3S.js} +1 -1
  17. package/dist/frontend/assets/{typescript-DIMXtHre.js → typescript-D9oav8M6.js} +1 -1
  18. package/dist/frontend/assets/{xml-DQ5HnppJ.js → xml-B0ks0e6Y.js} +1 -1
  19. package/dist/frontend/assets/{yaml-BQCOKj13.js → yaml-CCDt1oK4.js} +1 -1
  20. package/dist/frontend/index.html +2 -2
  21. package/dist/index.js +99 -90
  22. package/dist/secrets/index.js +74 -0
  23. package/dist/skills/index.js +43 -1
  24. package/dist/users/index.js +98 -0
  25. package/dist/utils/redact.js +52 -5
  26. package/dist/utils/resource-file-routes.js +2 -4
  27. package/dist/utils/resource-files.js +10 -2
  28. package/dist/utils/secret-contract-validation.js +184 -0
  29. package/dist/utils/secrets.js +127 -0
  30. package/dist/utils/skill-files.js +7 -0
  31. package/dist/utils/skill.js +50 -1
  32. package/dist/utils/workflow-runner.js +19 -4
  33. package/dist/utils/workflow.js +13 -1
  34. package/dist/workflows/index.js +10 -1
  35. package/dist/workspace_files/.opencode/plugins/createSkill.ts +1 -26
  36. package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +3 -3
  37. package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +93 -5
  38. package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +31 -49
  39. package/dist/workspace_files/.opencode/plugins/listAvailableSecretKeys.ts +107 -0
  40. package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +2 -2
  41. package/dist/workspace_files/.opencode/plugins/secret-proxy.ts +818 -0
  42. package/dist/workspace_files/AGENTS.md +91 -21
  43. package/package.json +5 -3
  44. package/prisma/generated/client/edge.js +24 -3
  45. package/prisma/generated/client/index-browser.js +21 -0
  46. package/prisma/generated/client/index.d.ts +3139 -128
  47. package/prisma/generated/client/index.js +24 -3
  48. package/prisma/generated/client/package.json +1 -1
  49. package/prisma/generated/client/schema.prisma +27 -0
  50. package/prisma/generated/client/wasm.js +24 -3
  51. package/prisma/migrations/20260402060129_add_secret_management/migration.sql +38 -0
  52. package/prisma/migrations/20260404052800_remove_global_secret_user_fkeys/migration.sql +20 -0
  53. package/prisma/schema.prisma +27 -0
  54. package/dist/frontend/assets/index-B8Ip8I8F.css +0 -1
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createApp = createApp;
6
7
  BigInt.prototype.toJSON = function () {
7
8
  const num = Number(this);
8
9
  if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) {
@@ -19,6 +20,7 @@ const workflows_1 = __importDefault(require("./workflows"));
19
20
  const chat_1 = __importDefault(require("./chat"));
20
21
  const skills_1 = __importDefault(require("./skills"));
21
22
  const users_1 = __importDefault(require("./users"));
23
+ const secrets_1 = __importDefault(require("./secrets"));
22
24
  const cronjob_1 = require("./cronjob");
23
25
  const opencode_server_1 = require("./opencode-server");
24
26
  const path_1 = __importDefault(require("path"));
@@ -30,97 +32,102 @@ const opencode_auth_1 = require("./utils/opencode-auth");
30
32
  const opencode_auth_2 = __importDefault(require("./opencode-auth"));
31
33
  const jwt_secret_1 = require("./utils/jwt-secret");
32
34
  const runtime_paths_1 = require("./utils/runtime-paths");
33
- const app = (0, express_1.default)();
34
- const frontendDistDirectory = (0, runtime_paths_1.getFrontendDistDirectory)();
35
- app.use(express_1.default.json());
36
- // Logging middleware
37
- // app.use((req, res, next) => {
38
- // const start = Date.now();
39
- // res.on('finish', async () => {
40
- // const duration = Date.now() - start;
41
- // const logData = {
42
- // method: req.method,
43
- // path: req.path,
44
- // statusCode: res.statusCode,
45
- // duration: `${duration}ms`,
46
- // userAgent: req.get('user-agent'),
47
- // ip: req.ip
48
- // };
49
- // logInfo(`HTTP Request: ${req.method} ${req.path} ${res.statusCode}`, { meta: logData });
50
- // });
51
- // next();
52
- // });
53
- // Mount auth routes directly (no sanitization for token responses)
54
- app.use('/api/auth', auth_1.default);
55
- const apiRouter = express_1.default.Router();
56
- apiRouter.use((_req, res, next) => {
57
- const originalJson = res.json.bind(res);
58
- const originalSend = res.send.bind(res);
59
- const shouldSkipSanitization = () => Boolean(res.locals.skipResponseSanitization);
60
- const hasJsonContentType = () => {
61
- const contentType = res.getHeader("Content-Type");
62
- if (typeof contentType === "string") {
63
- return contentType.includes("application/json");
64
- }
65
- if (Array.isArray(contentType)) {
66
- return contentType.some((value) => value.includes("application/json"));
67
- }
68
- return false;
69
- };
70
- res.json = ((body) => {
71
- if (shouldSkipSanitization()) {
72
- return originalJson(body);
73
- }
74
- return originalJson((0, redact_1.sanitizeForClient)(body));
75
- });
76
- res.send = ((body) => {
77
- if (shouldSkipSanitization()) {
78
- return originalSend(body);
79
- }
80
- if (typeof body === "string") {
81
- if (hasJsonContentType()) {
82
- return originalSend(body);
35
+ function createApp() {
36
+ const app = (0, express_1.default)();
37
+ const frontendDistDirectory = (0, runtime_paths_1.getFrontendDistDirectory)();
38
+ app.use(express_1.default.json());
39
+ // Logging middleware
40
+ // app.use((req, res, next) => {
41
+ // const start = Date.now();
42
+ // res.on('finish', async () => {
43
+ // const duration = Date.now() - start;
44
+ // const logData = {
45
+ // method: req.method,
46
+ // path: req.path,
47
+ // statusCode: res.statusCode,
48
+ // duration: `${duration}ms`,
49
+ // userAgent: req.get('user-agent'),
50
+ // ip: req.ip
51
+ // };
52
+ // logInfo(`HTTP Request: ${req.method} ${req.path} ${res.statusCode}`, { meta: logData });
53
+ // });
54
+ // next();
55
+ // });
56
+ // Mount auth routes directly (no sanitization for token responses)
57
+ app.use('/api/auth', auth_1.default);
58
+ const apiRouter = express_1.default.Router();
59
+ apiRouter.use((_req, res, next) => {
60
+ const originalJson = res.json.bind(res);
61
+ const originalSend = res.send.bind(res);
62
+ const shouldSkipSanitization = () => Boolean(res.locals.skipResponseSanitization);
63
+ const hasJsonContentType = () => {
64
+ const contentType = res.getHeader("Content-Type");
65
+ if (typeof contentType === "string") {
66
+ return contentType.includes("application/json");
83
67
  }
84
- return originalSend((0, redact_1.sanitizeStringContent)(body));
85
- }
86
- if (Buffer.isBuffer(body)) {
87
- if (hasJsonContentType()) {
68
+ if (Array.isArray(contentType)) {
69
+ return contentType.some((value) => value.includes("application/json"));
70
+ }
71
+ return false;
72
+ };
73
+ res.json = ((body) => {
74
+ if (shouldSkipSanitization()) {
75
+ return originalJson(body);
76
+ }
77
+ return originalJson((0, redact_1.sanitizeForClient)(body));
78
+ });
79
+ res.send = ((body) => {
80
+ if (shouldSkipSanitization()) {
88
81
  return originalSend(body);
89
82
  }
90
- return originalSend(Buffer.from((0, redact_1.sanitizeStringContent)(body.toString("utf-8")), "utf-8"));
83
+ if (typeof body === "string") {
84
+ if (hasJsonContentType()) {
85
+ return originalSend(body);
86
+ }
87
+ return originalSend((0, redact_1.sanitizeStringContent)(body));
88
+ }
89
+ if (Buffer.isBuffer(body)) {
90
+ if (hasJsonContentType()) {
91
+ return originalSend(body);
92
+ }
93
+ return originalSend(Buffer.from((0, redact_1.sanitizeStringContent)(body.toString("utf-8")), "utf-8"));
94
+ }
95
+ return originalSend((0, redact_1.sanitizeForClient)(body));
96
+ });
97
+ next();
98
+ });
99
+ apiRouter.get("/", (_req, res) => {
100
+ // for healthcheck
101
+ res.send("Hello from the API!");
102
+ });
103
+ apiRouter.get("/workspace", (0, utils_1.apiHandler)(async (_req, res) => {
104
+ const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
105
+ res.json({ workspace_dir: workspaceDir });
106
+ }, false));
107
+ apiRouter.use('/workflows', workflows_1.default);
108
+ apiRouter.use('/chat', chat_1.default);
109
+ apiRouter.use('/skills', skills_1.default);
110
+ apiRouter.use('/users', users_1.default);
111
+ apiRouter.use('/secrets', secrets_1.default);
112
+ apiRouter.use('/opencode-auth', opencode_auth_2.default);
113
+ app.use('/api', apiRouter);
114
+ // Serve static assets (JS, CSS, etc.) with correct MIME types
115
+ app.use(express_1.default.static(frontendDistDirectory));
116
+ // SPA fallback: serve index.html for non-API routes (client-side routing)
117
+ app.get("*", (_req, res) => {
118
+ res.sendFile(path_1.default.join(frontendDistDirectory, "index.html"));
119
+ });
120
+ app.use(async (err, req, res, _) => {
121
+ let status = err.status || 500;
122
+ let doLogging = err.doLogging !== false;
123
+ if (status !== 404 && doLogging) {
124
+ (0, logging_1.logError)({ err, apiPath: req.path, apiMethod: req.method });
91
125
  }
92
- return originalSend((0, redact_1.sanitizeForClient)(body));
126
+ res.status(status).json({ message: err.message || 'Unknown error' });
93
127
  });
94
- next();
95
- });
96
- apiRouter.get("/", (_req, res) => {
97
- // for healthcheck
98
- res.send("Hello from the API!");
99
- });
100
- apiRouter.get("/workspace", (0, utils_1.apiHandler)(async (_req, res) => {
101
- const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
102
- res.json({ workspace_dir: workspaceDir });
103
- }, false));
104
- apiRouter.use('/workflows', workflows_1.default);
105
- apiRouter.use('/chat', chat_1.default);
106
- apiRouter.use('/skills', skills_1.default);
107
- apiRouter.use('/users', users_1.default);
108
- apiRouter.use('/opencode-auth', opencode_auth_2.default);
109
- app.use('/api', apiRouter);
110
- // Serve static assets (JS, CSS, etc.) with correct MIME types
111
- app.use(express_1.default.static(frontendDistDirectory));
112
- // SPA fallback: serve index.html for non-API routes (client-side routing)
113
- app.get("*", (_req, res) => {
114
- res.sendFile(path_1.default.join(frontendDistDirectory, "index.html"));
115
- });
116
- app.use(async (err, req, res, _) => {
117
- let status = err.status || 500;
118
- let doLogging = err.doLogging !== false;
119
- if (status !== 404 && doLogging) {
120
- (0, logging_1.logError)({ err, apiPath: req.path, apiMethod: req.method });
121
- }
122
- res.status(status).json({ message: err.message || 'Unknown error' });
123
- });
128
+ return app;
129
+ }
130
+ const app = createApp();
124
131
  let httpServer = null;
125
132
  let isShuttingDown = false;
126
133
  async function shutdown(exitCode) {
@@ -163,7 +170,9 @@ process.on("unhandledRejection", (reason) => {
163
170
  console.error("Unhandled rejection:", reason);
164
171
  void shutdown(1);
165
172
  });
166
- void bootstrap().catch((err) => {
167
- console.error("Failed to start server:", err);
168
- process.exit(1);
169
- });
173
+ if (require.main === module) {
174
+ void bootstrap().catch((err) => {
175
+ console.error("Failed to start server:", err);
176
+ process.exit(1);
177
+ });
178
+ }
@@ -0,0 +1,74 @@
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 client_1 = __importDefault(require("../prisma/client"));
8
+ const utils_1 = require("../utils");
9
+ const secrets_1 = require("../utils/secrets");
10
+ const router = express_1.default.Router({ mergeParams: true });
11
+ router.get("/global", (0, utils_1.apiHandler)(async (req, res) => {
12
+ const rows = await client_1.default.global_secrets.findMany({
13
+ orderBy: { key: "asc" }
14
+ });
15
+ const shouldMaskValues = req.opencode_session_id === undefined;
16
+ res.json({
17
+ secrets: rows.map((row) => (0, secrets_1.toSecretListItem)(row, shouldMaskValues))
18
+ });
19
+ }, true));
20
+ router.put("/global/:key", (0, utils_1.apiHandler)(async (req, res) => {
21
+ if (req.role !== "Engineer") {
22
+ throw {
23
+ status: 403,
24
+ message: "Only engineers can manage global secrets"
25
+ };
26
+ }
27
+ const key = (0, secrets_1.assertSecretKey)(req.params.key);
28
+ const value = typeof req.body?.value === "string" ? req.body.value : "";
29
+ if (value.length === 0) {
30
+ throw {
31
+ status: 400,
32
+ message: "value is required"
33
+ };
34
+ }
35
+ const now = BigInt(Date.now());
36
+ const row = await client_1.default.global_secrets.upsert({
37
+ where: { key },
38
+ create: {
39
+ key,
40
+ value,
41
+ created_by_user_id: req.userId,
42
+ updated_by_user_id: req.userId,
43
+ created_at: now,
44
+ updated_at: now,
45
+ },
46
+ update: {
47
+ value,
48
+ updated_by_user_id: req.userId,
49
+ updated_at: now,
50
+ },
51
+ });
52
+ res.json({
53
+ secret: (0, secrets_1.toSecretListItem)(row, req.opencode_session_id === undefined)
54
+ });
55
+ }, true));
56
+ router.delete("/global/:key", (0, utils_1.apiHandler)(async (req, res) => {
57
+ if (req.role !== "Engineer") {
58
+ throw {
59
+ status: 403,
60
+ message: "Only engineers can manage global secrets"
61
+ };
62
+ }
63
+ const key = (0, secrets_1.assertSecretKey)(req.params.key);
64
+ await client_1.default.global_secrets.delete({
65
+ where: { key }
66
+ }).catch(() => {
67
+ throw {
68
+ status: 404,
69
+ message: `Global secret not found for key: ${key}`
70
+ };
71
+ });
72
+ res.json({ success: true });
73
+ }, true));
74
+ exports.default = router;
@@ -17,6 +17,8 @@ const skill_files_1 = require("../utils/skill-files");
17
17
  const resource_file_routes_1 = require("../utils/resource-file-routes");
18
18
  const skill_approval_snapshot_1 = require("../utils/skill-approval-snapshot");
19
19
  const resource_access_1 = require("../utils/resource-access");
20
+ const secrets_1 = require("../utils/secrets");
21
+ const secret_contract_validation_1 = require("../utils/secret-contract-validation");
20
22
  const router = express_1.default.Router({ mergeParams: true });
21
23
  const uploadTmpDir = path_1.default.join(os_1.default.tmpdir(), "teamcopilot-skill-uploads");
22
24
  fs_1.default.mkdirSync(uploadTmpDir, { recursive: true });
@@ -77,6 +79,7 @@ router.get("/", (0, index_1.apiHandler)(async (req, res) => {
77
79
  const slugs = (0, skill_1.listSkillSlugs)();
78
80
  const skills = [];
79
81
  const creatorIds = new Set();
82
+ const resolvedSecrets = await (0, secrets_1.listResolvedSecretsForUser)(req.userId);
80
83
  const metadataBySlug = new Map();
81
84
  for (const slug of slugs) {
82
85
  const metadata = await (0, skill_1.getOrCreateSkillMetadataAndEnsurePermission)(slug);
@@ -122,6 +125,8 @@ router.get("/", (0, index_1.apiHandler)(async (req, res) => {
122
125
  can_edit: accessSummary.can_edit,
123
126
  permission_mode: accessSummary.permission_mode,
124
127
  is_locked_due_to_missing_users: accessSummary.is_locked_due_to_missing_users,
128
+ required_secrets: manifest.required_secrets,
129
+ missing_required_secrets: (0, secrets_1.resolveSecretsFromResolvedMap)(resolvedSecrets, manifest.required_secrets).missingKeys,
125
130
  });
126
131
  }
127
132
  res.json({ skills });
@@ -144,6 +149,7 @@ router.get("/:slug", (0, index_1.apiHandler)(async (req, res) => {
144
149
  const approver = approvedByUserId ? (usersById.get(approvedByUserId) ?? null) : null;
145
150
  const accessSummary = await (0, resource_access_1.getResourceAccessSummary)("skill", slug, req.userId);
146
151
  const permission = await (0, skill_permissions_1.getSkillAccessPermissionWithUsers)(slug);
152
+ const secretResolution = await (0, secrets_1.resolveSecretsForUser)(req.userId, manifest.required_secrets);
147
153
  const permissionMode = (0, permission_common_1.assertCommonPermissionMode)(permission.permission_mode, "skill access");
148
154
  const permissions = permissionMode === "everyone"
149
155
  ? { mode: "everyone" }
@@ -163,6 +169,8 @@ router.get("/:slug", (0, index_1.apiHandler)(async (req, res) => {
163
169
  can_edit: accessSummary.can_edit,
164
170
  permission_mode: accessSummary.permission_mode,
165
171
  is_locked_due_to_missing_users: accessSummary.is_locked_due_to_missing_users,
172
+ required_secrets: manifest.required_secrets,
173
+ missing_required_secrets: secretResolution.missingKeys,
166
174
  permissions,
167
175
  allowed_users_resolved: permission.allowedUsers.map((row) => ({
168
176
  user_id: row.user.id,
@@ -174,6 +182,40 @@ router.get("/:slug", (0, index_1.apiHandler)(async (req, res) => {
174
182
  }
175
183
  });
176
184
  }, true));
185
+ router.get("/:slug/runtime-content", (0, index_1.apiHandler)(async (req, res) => {
186
+ const slug = req.params.slug;
187
+ await assertCanViewSkillFiles(slug, req.userId);
188
+ const { manifest } = await (0, skill_1.readSkillManifestAndEnsurePermissions)(slug);
189
+ const accessSummary = await (0, resource_access_1.getResourceAccessSummary)("skill", slug, req.userId);
190
+ if (!accessSummary.is_approved) {
191
+ throw {
192
+ status: 403,
193
+ message: `Skill "${slug}" is not approved yet. Only approved skills can be read through getSkillContent.`
194
+ };
195
+ }
196
+ const skillContent = await (0, skill_files_1.readSkillFileContent)(slug, "SKILL.md");
197
+ if (skillContent.kind !== "text") {
198
+ throw {
199
+ status: 500,
200
+ message: "SKILL.md must be a text file"
201
+ };
202
+ }
203
+ (0, secret_contract_validation_1.validateSkillSecretContract)(skillContent.content ?? "");
204
+ const secretResolution = await (0, secrets_1.resolveSecretsForUser)(req.userId, manifest.required_secrets);
205
+ if (secretResolution.missingKeys.length > 0) {
206
+ throw {
207
+ status: 400,
208
+ message: `I can't use skill "${slug}" because these required secrets are missing: ${secretResolution.missingKeys.join(", ")}. Ask the user to add these keys in TeamCopilot Profile Secrets before using this skill.`
209
+ };
210
+ }
211
+ res.json({
212
+ skill: {
213
+ slug,
214
+ path: skillContent.path,
215
+ content: skillContent.content ?? "",
216
+ }
217
+ });
218
+ }, true));
177
219
  router.post("/:slug/approve", (0, index_1.apiHandler)(async (req, res) => {
178
220
  const slug = req.params.slug;
179
221
  const approvalResult = await (0, skill_approval_snapshot_1.approveSkillWithSnapshot)(slug, req.userId);
@@ -212,6 +254,7 @@ router.get("/:slug/approval-diff", (0, index_1.apiHandler)(async (req, res) => {
212
254
  const previousSnapshot = await (0, skill_approval_snapshot_1.loadApprovedSkillSnapshotFromDb)(slug);
213
255
  const currentSnapshot = (0, skill_approval_snapshot_1.collectCurrentSkillSnapshot)(slug);
214
256
  const diff = (0, skill_approval_snapshot_1.buildSkillApprovalDiffResponse)(previousSnapshot, currentSnapshot);
257
+ res.locals.skipResponseSanitization = true;
215
258
  res.json(diff);
216
259
  }, true));
217
260
  (0, resource_file_routes_1.registerResourceFileRoutes)({
@@ -230,7 +273,6 @@ router.get("/:slug/approval-diff", (0, index_1.apiHandler)(async (req, res) => {
230
273
  uploadFileFromTempPath: skill_files_1.uploadSkillFileFromTempPath,
231
274
  renamePath: skill_files_1.renameSkillPath,
232
275
  deletePath: skill_files_1.deleteSkillPath,
233
- skipResponseSanitizationForFileContentRead: false,
234
276
  });
235
277
  const updateSkillPermissionsHandler = (0, index_1.apiHandler)(async (req, res) => {
236
278
  const slug = req.params.slug;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const express_1 = __importDefault(require("express"));
7
7
  const client_1 = __importDefault(require("../prisma/client"));
8
8
  const index_1 = require("../utils/index");
9
+ const secrets_1 = require("../utils/secrets");
9
10
  const router = express_1.default.Router({ mergeParams: true });
10
11
  router.get("/", (0, index_1.apiHandler)(async (_req, res) => {
11
12
  const users = await client_1.default.users.findMany({
@@ -19,4 +20,101 @@ router.get("/", (0, index_1.apiHandler)(async (_req, res) => {
19
20
  });
20
21
  res.json({ users });
21
22
  }, true));
23
+ router.get("/me/secrets", (0, index_1.apiHandler)(async (req, res) => {
24
+ const rows = await client_1.default.user_secrets.findMany({
25
+ where: {
26
+ user_id: req.userId,
27
+ },
28
+ orderBy: { key: "asc" }
29
+ });
30
+ const shouldMaskValues = req.opencode_session_id === undefined;
31
+ res.json({
32
+ secrets: rows.map((row) => (0, secrets_1.toSecretListItem)(row, shouldMaskValues))
33
+ });
34
+ }, true));
35
+ router.get("/me/resolved-secrets", (0, index_1.apiHandler)(async (req, res) => {
36
+ const resolvedSecretMap = await (0, secrets_1.listResolvedSecretsForUser)(req.userId);
37
+ res.json({
38
+ secret_keys: Object.keys(resolvedSecretMap),
39
+ total: Object.keys(resolvedSecretMap).length,
40
+ });
41
+ }, true));
42
+ router.post("/me/resolve-secrets", (0, index_1.apiHandler)(async (req, res) => {
43
+ if (req.opencode_session_id === undefined) {
44
+ throw {
45
+ status: 403,
46
+ message: "This endpoint requires an opencode session token"
47
+ };
48
+ }
49
+ const rawKeys = Array.isArray(req.body?.keys) ? req.body.keys : [];
50
+ const requestedKeys = rawKeys
51
+ .filter((key) => typeof key === "string")
52
+ .map((key) => (0, secrets_1.assertSecretKey)(key));
53
+ if (requestedKeys.length === 0) {
54
+ throw {
55
+ status: 400,
56
+ message: "keys is required"
57
+ };
58
+ }
59
+ const resolution = await (0, secrets_1.resolveSecretsForUser)(req.userId, requestedKeys);
60
+ if (resolution.missingKeys.length > 0) {
61
+ throw {
62
+ status: 400,
63
+ message: `This command references missing secrets: ${resolution.missingKeys.join(", ")}. Ask the user to add these keys in TeamCopilot Profile Secrets before retrying.`
64
+ };
65
+ }
66
+ res.json({
67
+ secret_map: resolution.secretMap,
68
+ });
69
+ }, true));
70
+ router.put("/me/secrets/:key", (0, index_1.apiHandler)(async (req, res) => {
71
+ const key = (0, secrets_1.assertSecretKey)(req.params.key);
72
+ const value = typeof req.body?.value === "string" ? req.body.value : "";
73
+ if (value.length === 0) {
74
+ throw {
75
+ status: 400,
76
+ message: "value is required"
77
+ };
78
+ }
79
+ const now = BigInt(Date.now());
80
+ const row = await client_1.default.user_secrets.upsert({
81
+ where: {
82
+ user_id_key: {
83
+ user_id: req.userId,
84
+ key,
85
+ }
86
+ },
87
+ create: {
88
+ user_id: req.userId,
89
+ key,
90
+ value,
91
+ created_at: now,
92
+ updated_at: now,
93
+ },
94
+ update: {
95
+ value,
96
+ updated_at: now,
97
+ }
98
+ });
99
+ res.json({
100
+ secret: (0, secrets_1.toSecretListItem)(row, req.opencode_session_id === undefined)
101
+ });
102
+ }, true));
103
+ router.delete("/me/secrets/:key", (0, index_1.apiHandler)(async (req, res) => {
104
+ const key = (0, secrets_1.assertSecretKey)(req.params.key);
105
+ await client_1.default.user_secrets.delete({
106
+ where: {
107
+ user_id_key: {
108
+ user_id: req.userId,
109
+ key,
110
+ }
111
+ }
112
+ }).catch(() => {
113
+ throw {
114
+ status: 404,
115
+ message: `Secret not found for key: ${key}`
116
+ };
117
+ });
118
+ res.json({ success: true });
119
+ }, true));
22
120
  exports.default = router;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sanitizeStringContent = sanitizeStringContent;
4
4
  exports.sanitizeForClient = sanitizeForClient;
5
5
  const SENSITIVE_KEY_PATTERN = /(token|secret|password|passwd|api[_-]?key|auth|credential)/i;
6
+ const SECRET_PLACEHOLDER_PATTERN = /\{\{SECRET:[A-Z][A-Z0-9_]*\}\}/g;
6
7
  function maskValue(value) {
7
8
  if (value.startsWith("***")) {
8
9
  return value;
@@ -14,14 +15,40 @@ function maskValue(value) {
14
15
  function isLikelySensitiveKey(key) {
15
16
  return SENSITIVE_KEY_PATTERN.test(key);
16
17
  }
18
+ function isProtectedSecretPlaceholderToken(value) {
19
+ return /__TCPLH_\d+__/.test(value.trim());
20
+ }
21
+ function isSecretPlaceholderValue(value) {
22
+ return value.trim().match(/^\{\{SECRET:[A-Z][A-Z0-9_]*\}\}$/) !== null;
23
+ }
24
+ function protectSecretPlaceholders(input) {
25
+ const placeholders = [];
26
+ const protectedText = input.replace(SECRET_PLACEHOLDER_PATTERN, (match) => {
27
+ const index = placeholders.push(match) - 1;
28
+ return `__TCPLH_${index}__`;
29
+ });
30
+ return {
31
+ protectedText,
32
+ restore: (value) => value.replace(/__TCPLH_(\d+)__/g, (_full, rawIndex) => {
33
+ const index = Number(rawIndex);
34
+ return placeholders[index] ?? _full;
35
+ }),
36
+ };
37
+ }
17
38
  function sanitizeStringContent(input) {
18
- let text = input;
39
+ const { protectedText, restore } = protectSecretPlaceholders(input);
40
+ let text = protectedText;
19
41
  // Redact credentials embedded in URLs such as:
20
42
  // https://user:password@host/path
21
43
  text = text.replace(/\b(https?:\/\/)([^\/\s"'@:]+):([^\/\s"'@]+)@/gi, (_full, scheme, username, password) => `${scheme}${username}:${maskValue(password)}@`);
22
44
  // Redact Authorization header bearer tokens before generic key/value masking so
23
45
  // "Authorization: Bearer <token>" doesn't get partially masked as "***rer".
24
- text = text.replace(/(\bAuthorization\s*[:=]\s*)(Bearer\s+)([A-Za-z0-9._~+/=-]{8,})/gi, (_full, prefix, bearer, token) => `${prefix}${bearer}${maskValue(token)}`);
46
+ text = text.replace(/(\bAuthorization\s*[:=]\s*)(Bearer\s+)([A-Za-z0-9._~+/=-]{8,})/gi, (_full, prefix, bearer, token) => {
47
+ if (isProtectedSecretPlaceholderToken(token)) {
48
+ return `${prefix}${bearer}${token}`;
49
+ }
50
+ return `${prefix}${bearer}${maskValue(token)}`;
51
+ });
25
52
  // Redact sensitive markdown list/label items such as:
26
53
  // - **password**: value
27
54
  // - **password** = value
@@ -34,6 +61,9 @@ function sanitizeStringContent(input) {
34
61
  if (!rawValue) {
35
62
  return full;
36
63
  }
64
+ if (isProtectedSecretPlaceholderToken(rawValue)) {
65
+ return full;
66
+ }
37
67
  const masked = maskValue(rawValue);
38
68
  if (doubleQuoted !== undefined) {
39
69
  return `${lineLead}${prefix}${openMarker}${key}${closeMarker}${separator}"${masked}"`;
@@ -54,6 +84,9 @@ function sanitizeStringContent(input) {
54
84
  if (!rawValue) {
55
85
  return full;
56
86
  }
87
+ if (isProtectedSecretPlaceholderToken(rawValue)) {
88
+ return full;
89
+ }
57
90
  const masked = maskValue(rawValue);
58
91
  if (doubleQuoted !== undefined) {
59
92
  return `${lead}${assignmentPrefix}"${masked}"`;
@@ -73,6 +106,9 @@ function sanitizeStringContent(input) {
73
106
  if (!rawValue) {
74
107
  return full;
75
108
  }
109
+ if (isProtectedSecretPlaceholderToken(rawValue)) {
110
+ return full;
111
+ }
76
112
  if (key.toLowerCase() === "authorization" && /^bearer$/i.test(rawValue)) {
77
113
  return full;
78
114
  }
@@ -80,6 +116,9 @@ function sanitizeStringContent(input) {
80
116
  if (key.toLowerCase() === "authorization" && authorizationMatch) {
81
117
  const bearerPrefix = authorizationMatch[1];
82
118
  const bearerToken = authorizationMatch[2];
119
+ if (isProtectedSecretPlaceholderToken(bearerToken)) {
120
+ return full;
121
+ }
83
122
  const maskedBearer = `${bearerPrefix}${maskValue(bearerToken)}`;
84
123
  if (doubleQuoted !== undefined) {
85
124
  return `${lead}${assignmentPrefix}"${maskedBearer}"`;
@@ -99,15 +138,23 @@ function sanitizeStringContent(input) {
99
138
  return `${lead}${assignmentPrefix}${masked}`;
100
139
  });
101
140
  // Redact common bearer and provider token forms.
102
- text = text.replace(/\b(Bearer\s+)([A-Za-z0-9._~+/=-]{8,})\b/gi, (_full, prefix, token) => `${prefix}${maskValue(token)}`);
141
+ text = text.replace(/\b(Bearer\s+)([A-Za-z0-9._~+/=-]{8,})\b/gi, (_full, prefix, token) => {
142
+ if (isProtectedSecretPlaceholderToken(token)) {
143
+ return `${prefix}${token}`;
144
+ }
145
+ return `${prefix}${maskValue(token)}`;
146
+ });
103
147
  text = text.replace(/\b(sk-[A-Za-z0-9_-]{8,})\b/g, (token) => maskValue(token));
104
148
  text = text.replace(/\b(ghp_[A-Za-z0-9]{8,})\b/g, (token) => maskValue(token));
105
- return text;
149
+ return restore(text);
106
150
  }
107
151
  function sanitizeObjectRecord(input) {
108
152
  const output = {};
109
153
  for (const [key, value] of Object.entries(input)) {
110
- if (typeof value === "string" && isLikelySensitiveKey(key)) {
154
+ if (typeof value === "string"
155
+ && isLikelySensitiveKey(key)
156
+ && !isProtectedSecretPlaceholderToken(value)
157
+ && !isSecretPlaceholderValue(value)) {
111
158
  output[key] = maskValue(value);
112
159
  continue;
113
160
  }
@@ -7,7 +7,7 @@ exports.registerResourceFileRoutes = registerResourceFileRoutes;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const index_1 = require("./index");
9
9
  function registerResourceFileRoutes(options) {
10
- const { router, uploadMiddleware, ensureResourceExists, assertCanView, getEditorAccess, assertCanEdit, listDirectory, readFileContent, saveFileContent, createFileOrFolder, uploadFileFromTempPath, renamePath, deletePath, skipResponseSanitizationForFileContentRead = true, } = options;
10
+ const { router, uploadMiddleware, ensureResourceExists, assertCanView, getEditorAccess, assertCanEdit, listDirectory, readFileContent, saveFileContent, createFileOrFolder, uploadFileFromTempPath, renamePath, deletePath, } = options;
11
11
  router.get("/:slug/files/access", (0, index_1.apiHandler)(async (req, res) => {
12
12
  const slug = req.params.slug;
13
13
  const authReq = req;
@@ -30,9 +30,7 @@ function registerResourceFileRoutes(options) {
30
30
  await ensureResourceExists(slug);
31
31
  const rawPath = typeof req.query.path === "string" ? req.query.path : undefined;
32
32
  const content = readFileContent(slug, rawPath);
33
- if (skipResponseSanitizationForFileContentRead) {
34
- res.locals.skipResponseSanitization = true;
35
- }
33
+ res.locals.skipResponseSanitization = true;
36
34
  res.json(content);
37
35
  }, true));
38
36
  router.put("/:slug/files/content", (0, index_1.apiHandler)(async (req, res) => {
@@ -10,7 +10,7 @@ const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const redact_1 = require("./redact");
12
12
  function createResourceFileManager(options) {
13
- const { getResourcePath, resourceLabel, editorLabel } = options;
13
+ const { getResourcePath, resourceLabel, editorLabel, validateBeforeSave } = options;
14
14
  const rootLabel = `${resourceLabel} root`;
15
15
  const symlinkError = `${editorLabel} editor does not support symlinks`;
16
16
  function getResolvedRootPaths(slug) {
@@ -345,10 +345,18 @@ function createResourceFileManager(options) {
345
345
  }
346
346
  const name = path_1.default.basename(relativePath);
347
347
  const currentRawContent = currentBytes.toString("utf-8");
348
- const wasServedRedacted = name === ".env" || resourceLabel === "skill";
348
+ const wasServedRedacted = name === ".env";
349
349
  const nextContent = wasServedRedacted
350
350
  ? mergeRedactedEditorContent(currentRawContent, request.content)
351
351
  : request.content;
352
+ if (validateBeforeSave) {
353
+ validateBeforeSave({
354
+ slug,
355
+ resourcePath: getResourcePath(slug),
356
+ relativePath,
357
+ nextContent,
358
+ });
359
+ }
352
360
  fs_1.default.writeFileSync(absolutePath, nextContent, "utf-8");
353
361
  const nextBytes = fs_1.default.readFileSync(absolutePath);
354
362
  const nextStat = fs_1.default.statSync(absolutePath);